[FEATURE]:Support Gemini models with Function Calling through incompatible OpenAI-compatible proxies #8147

Open
opened 2026-02-16 18:09:16 -05:00 by yindo · 1 comment
Owner

Originally created by @JoelJeffery-ux on GitHub (Jan 31, 2026).

Originally assigned to: @thdxr on GitHub.

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Problem Summary:
OpenCode cannot successfully use Gemini models through certain OpenAI-compatible proxy APIs (e.g., api.v3.cm/v1/) when Function Calling is enabled. The API returns validation errors due to JSON Schema fields that are not supported by the proxy.

Error Messages:

Invalid JSON payload received. Unknown name "ref" at 'tools[0].function declarations[0].***.***.properties[0].***.***.properties[2].***.items': Cannot find field.
Invalid JSON payload received. Unknown name "propertyNames" at 'tools[0].function declarations[28].***.properties[0].value.any_of[1]': Cannot find field.

Root Cause:
OpenCode generates complete JSON Schema for Function Calling (using Vercel AI SDK), including advanced validation fields such as:

  • $ref / ref
  • propertyNames
  • additionalProperties
  • allOf, anyOf, oneOf, not
  • patternProperties

However, many OpenAI-compatible proxy APIs do not support these fields and reject requests containing them, returning "Unknown name" errors.

Suggested Enhancement:

Option 1 (Preferred): Add JSON Schema Compatibility Mode

  • Add a provider-level configuration option (e.g., compatibilityMode: "strict" or simplifiedSchema: true) that instructs OpenCode to generate minimal JSON Schema compatible with basic OpenAI API spec
  • When enabled, exclude advanced validation fields and only include: type, properties, items, required, description, enum, format

Option 2: Allow Custom Schema Sanitization Hook

  • Expose a webhook or script hook that allows users to preprocess request bodies before sending to the API
  • This enables users to implement their own schema cleaning logic without modifying OpenCode's core

Option 3: Document Workaround with Local Proxy

  • Include official documentation for setting up a local proxy server that sanitizes JSON Schema before forwarding to incompatible APIs
  • Provide example proxy server code and configuration templates

Additional Context:

  • OpenCode is a compiled binary, so users cannot modify source code to work around this issue
  • This specifically affects Gemini models and other models that heavily use Function Calling
  • The workaround currently requires running a custom Node.js proxy server between OpenCode and the API (see attached solution guide)

Impact:

  • Users cannot use Gemini models with Function Calling through incompatible proxy APIs
  • Forces users to either switch providers or maintain custom proxy infrastructure
  • Limits flexibility in API provider selection

Use Case:
Users want to use OpenCode with proxy APIs that have incomplete OpenAI compatibility, specifically for Gemini models that require Function Calling capabilities.


Proposed Workaround Solution

Since OpenCode is a compiled binary, I've implemented a local proxy server workaround that successfully resolves this issue. This solution filters incompatible JSON Schema fields before forwarding requests to the API.

Architecture

���─────────────┐
│  OpenCode   │
│  (Binary)   │
└──────┬──────┘
       │
       │ 1. Send request (with full JSON Schema)
       │
       ▼
┌─────────────────┐
│  Local Proxy    │
│  (Node.js)      │
└──────┬──────────┘
       │
       │ 2. Clean incompatible fields
       │
       ▼
┌─────────────────┐
│  Proxy API      │
│  (api.v3.cm)    │
└─────────────────┘

Proxy Server Implementation

File: proxy-server.js

#!/usr/bin/env node

/**
 * OpenCode Compatibility Proxy Server
 * Filters incompatible JSON Schema fields for certain proxy APIs
 */

const http = require('http');
const https = require('https');
const url = require('url');
const zlib = require('zlib');

const TARGET_API = 'https://api.v3.cm/v1/';
const API_KEY = 'your_api_key_here';
const PORT = 8080;

/**
 * Recursively clean JSON Schema, removing incompatible fields
 */
function cleanSchema(obj) {
  if (!obj || typeof obj !== 'object') {
    return obj;
  }

  // Remove incompatible fields
  const incompatibleFields = ['$ref', 'ref', 'propertyNames', 'patternProperties', 'additionalProperties', 'allOf', 'anyOf', 'oneOf', 'not'];
  
  if (Array.isArray(obj)) {
    return obj.map(item => cleanSchema(item));
  }

  const cleaned = {};
  for (const key in obj) {
    if (incompatibleFields.includes(key)) {
      console.log(`[Filter] Removing incompatible field: ${key}`);
      continue;
    }
    
    // Special handling for properties
    if (key === 'properties' && typeof obj[key] === 'object') {
      cleaned[key] = {};
      for (const prop in obj[key]) {
        cleaned[key][prop] = cleanSchema(obj[key][prop]);
      }
      continue;
    }
    
    // Special handling for items
    if (key === 'items') {
      cleaned[key] = cleanSchema(obj[key]);
      continue;
    }
    
    // Recursively process nested objects
    cleaned[key] = cleanSchema(obj[key]);
  }
  
  return cleaned;
}

/**
 * Clean function declarations in tools array
 */
function cleanTools(body) {
  if (!body.tools) {
    return body;
  }

  console.log('[Clean] Found tools field, starting cleanup...');
  
  const cleanedTools = body.tools.map(tool => {
    if (tool.function && tool.function.parameters) {
      console.log(`[Clean] Processing function: ${tool.function.name}`);
      return {
        ...tool,
        function: {
          ...tool.function,
          parameters: cleanSchema(tool.function.parameters)
        }
      };
    }
    return tool;
  });

  return {
    ...body,
    tools: cleanedTools
  };
}

/**
 * Handle requests
 */
function handleRequest(req, res) {
  let body = '';
  
  req.on('data', chunk => {
    body += chunk.toString();
  });
  
  req.on('end', () => {
    try {
      const parsedBody = JSON.parse(body);
      console.log(`[Request] Received ${req.method} ${req.url}`);
      
      // Clean incompatible fields
      const cleanedBody = cleanTools(parsedBody);
      
      // Forward to target API
      const targetUrl = url.parse(TARGET_API + req.url.slice(1));
      const options = {
        hostname: targetUrl.hostname,
        port: targetUrl.port || 443,
        path: targetUrl.path,
        method: req.method,
        rejectUnauthorized: false,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${API_KEY}`,
          'Accept-Encoding': 'identity',
          'User-Agent': 'OpenCode-Proxy/1.0'
        }
      };
      
      const proxyReq = https.request(options, (proxyRes) => {
        const chunks = [];
        
        proxyRes.on('data', chunk => {
          chunks.push(chunk);
        });
        
        proxyRes.on('end', () => {
          console.log(`[Response] Status: ${proxyRes.statusCode}`);
          
          const buffer = Buffer.concat(chunks);
          let responseBody = buffer.toString('utf-8');
          
          // Handle compressed responses
          const encoding = proxyRes.headers['content-encoding'];
          if (encoding === 'gzip' || encoding === 'deflate') {
            try {
              if (encoding === 'gzip') {
                responseBody = zlib.gunzipSync(buffer).toString('utf-8');
              } else if (encoding === 'deflate') {
                responseBody = zlib.inflateSync(buffer).toString('utf-8');
              }
            } catch (decompressError) {
              responseBody = buffer.toString('utf-8');
            }
          }
          
          const responseHeaders = { ...proxyRes.headers };
          delete responseHeaders['content-encoding'];
          delete responseHeaders['content-length'];
          delete responseHeaders['transfer-encoding'];
          
          res.writeHead(proxyRes.statusCode, responseHeaders);
          res.end(responseBody);
        });
      });
      
      proxyReq.on('error', (error) => {
        console.error('[Error] Proxy request failed:', error);
        res.writeHead(500, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Proxy request failed', details: error.message }));
      });
      
      proxyReq.write(JSON.stringify(cleanedBody));
      proxyReq.end();
      
    } catch (error) {
      console.error('[Error] Failed to parse request:', error);
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Invalid JSON', details: error.message }));
    }
  });
}

// Start server
const server = http.createServer(handleRequest);
server.listen(PORT, '127.0.0.1', () => {
  console.log(`[Startup] Proxy server running on http://127.0.0.1:${PORT}`);
  console.log(`[Config] Target API: ${TARGET_API}`);
});

OpenCode Configuration

File: opencode.json

{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "proxy": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "proxy",
      "options": {
        "baseURL": "http://127.0.0.1:8080/",
        "apiKey": "local-proxy"
      },
      "models": {
        "gemini-3-pro-preview": {
          "name": "Gemini-3-Pro-Preview",
          "attachment": true,
          "limit": { "context": 1048576, "output": 65535 },
          "modalities": { "input": ["text"], "output": ["text"] }
        }
      }
    }
  }
}

Usage

  1. Start the proxy server:

    node proxy-server.js
    
  2. Start OpenCode:

    opencode
    
  3. Verify functionality:

    • The proxy will log filtered fields
    • Function Calling should work correctly with Gemini models

Key Features

  • Recursive Schema Cleaning - Deep traversal of JSON Schema
  • Compression Handling - Automatic detection and decompression
  • SSL Certificate Bypass - Works with self-signed certificates
  • Detailed Logging - Easy debugging and troubleshooting
  • Transparent Proxy - Works seamlessly with OpenCode

Limitations

  • Requires maintaining a separate Node.js process
  • Adds slight latency due to proxy layer
  • Not suitable for production environments without additional security measures

Note: This workaround has been tested and verified to work with api.v3.cm/v1/ and Gemini models with Function Calling enabled.

Originally created by @JoelJeffery-ux on GitHub (Jan 31, 2026). Originally assigned to: @thdxr on GitHub. ### Feature hasn't been suggested before. - [x] I have verified this feature I'm about to request hasn't been suggested before. ### Describe the enhancement you want to request **Problem Summary:** OpenCode cannot successfully use Gemini models through certain OpenAI-compatible proxy APIs (e.g., `api.v3.cm/v1/`) when Function Calling is enabled. The API returns validation errors due to JSON Schema fields that are not supported by the proxy. **Error Messages:** ``` Invalid JSON payload received. Unknown name "ref" at 'tools[0].function declarations[0].***.***.properties[0].***.***.properties[2].***.items': Cannot find field. Invalid JSON payload received. Unknown name "propertyNames" at 'tools[0].function declarations[28].***.properties[0].value.any_of[1]': Cannot find field. ``` **Root Cause:** OpenCode generates complete JSON Schema for Function Calling (using Vercel AI SDK), including advanced validation fields such as: - `$ref` / `ref` - `propertyNames` - `additionalProperties` - `allOf`, `anyOf`, `oneOf`, `not` - `patternProperties` However, many OpenAI-compatible proxy APIs do not support these fields and reject requests containing them, returning "Unknown name" errors. **Suggested Enhancement:** Option 1 (Preferred): **Add JSON Schema Compatibility Mode** - Add a provider-level configuration option (e.g., `compatibilityMode: "strict"` or `simplifiedSchema: true`) that instructs OpenCode to generate minimal JSON Schema compatible with basic OpenAI API spec - When enabled, exclude advanced validation fields and only include: `type`, `properties`, `items`, `required`, `description`, `enum`, `format` Option 2: **Allow Custom Schema Sanitization Hook** - Expose a webhook or script hook that allows users to preprocess request bodies before sending to the API - This enables users to implement their own schema cleaning logic without modifying OpenCode's core Option 3: **Document Workaround with Local Proxy** - Include official documentation for setting up a local proxy server that sanitizes JSON Schema before forwarding to incompatible APIs - Provide example proxy server code and configuration templates **Additional Context:** - OpenCode is a compiled binary, so users cannot modify source code to work around this issue - This specifically affects Gemini models and other models that heavily use Function Calling - The workaround currently requires running a custom Node.js proxy server between OpenCode and the API (see attached solution guide) **Impact:** - Users cannot use Gemini models with Function Calling through incompatible proxy APIs - Forces users to either switch providers or maintain custom proxy infrastructure - Limits flexibility in API provider selection **Use Case:** Users want to use OpenCode with proxy APIs that have incomplete OpenAI compatibility, specifically for Gemini models that require Function Calling capabilities. ------ ## Proposed Workaround Solution Since OpenCode is a compiled binary, I've implemented a local proxy server workaround that successfully resolves this issue. This solution filters incompatible JSON Schema fields before forwarding requests to the API. ### Architecture ``` ���─────────────┐ │ OpenCode │ │ (Binary) │ └──────┬──────┘ │ │ 1. Send request (with full JSON Schema) │ ▼ ┌─────────────────┐ │ Local Proxy │ │ (Node.js) │ └──────┬──────────┘ │ │ 2. Clean incompatible fields │ ▼ ┌─────────────────┐ │ Proxy API │ │ (api.v3.cm) │ └─────────────────┘ ``` ### Proxy Server Implementation **File: `proxy-server.js`** ```javascript #!/usr/bin/env node /** * OpenCode Compatibility Proxy Server * Filters incompatible JSON Schema fields for certain proxy APIs */ const http = require('http'); const https = require('https'); const url = require('url'); const zlib = require('zlib'); const TARGET_API = 'https://api.v3.cm/v1/'; const API_KEY = 'your_api_key_here'; const PORT = 8080; /** * Recursively clean JSON Schema, removing incompatible fields */ function cleanSchema(obj) { if (!obj || typeof obj !== 'object') { return obj; } // Remove incompatible fields const incompatibleFields = ['$ref', 'ref', 'propertyNames', 'patternProperties', 'additionalProperties', 'allOf', 'anyOf', 'oneOf', 'not']; if (Array.isArray(obj)) { return obj.map(item => cleanSchema(item)); } const cleaned = {}; for (const key in obj) { if (incompatibleFields.includes(key)) { console.log(`[Filter] Removing incompatible field: ${key}`); continue; } // Special handling for properties if (key === 'properties' && typeof obj[key] === 'object') { cleaned[key] = {}; for (const prop in obj[key]) { cleaned[key][prop] = cleanSchema(obj[key][prop]); } continue; } // Special handling for items if (key === 'items') { cleaned[key] = cleanSchema(obj[key]); continue; } // Recursively process nested objects cleaned[key] = cleanSchema(obj[key]); } return cleaned; } /** * Clean function declarations in tools array */ function cleanTools(body) { if (!body.tools) { return body; } console.log('[Clean] Found tools field, starting cleanup...'); const cleanedTools = body.tools.map(tool => { if (tool.function && tool.function.parameters) { console.log(`[Clean] Processing function: ${tool.function.name}`); return { ...tool, function: { ...tool.function, parameters: cleanSchema(tool.function.parameters) } }; } return tool; }); return { ...body, tools: cleanedTools }; } /** * Handle requests */ function handleRequest(req, res) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { try { const parsedBody = JSON.parse(body); console.log(`[Request] Received ${req.method} ${req.url}`); // Clean incompatible fields const cleanedBody = cleanTools(parsedBody); // Forward to target API const targetUrl = url.parse(TARGET_API + req.url.slice(1)); const options = { hostname: targetUrl.hostname, port: targetUrl.port || 443, path: targetUrl.path, method: req.method, rejectUnauthorized: false, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}`, 'Accept-Encoding': 'identity', 'User-Agent': 'OpenCode-Proxy/1.0' } }; const proxyReq = https.request(options, (proxyRes) => { const chunks = []; proxyRes.on('data', chunk => { chunks.push(chunk); }); proxyRes.on('end', () => { console.log(`[Response] Status: ${proxyRes.statusCode}`); const buffer = Buffer.concat(chunks); let responseBody = buffer.toString('utf-8'); // Handle compressed responses const encoding = proxyRes.headers['content-encoding']; if (encoding === 'gzip' || encoding === 'deflate') { try { if (encoding === 'gzip') { responseBody = zlib.gunzipSync(buffer).toString('utf-8'); } else if (encoding === 'deflate') { responseBody = zlib.inflateSync(buffer).toString('utf-8'); } } catch (decompressError) { responseBody = buffer.toString('utf-8'); } } const responseHeaders = { ...proxyRes.headers }; delete responseHeaders['content-encoding']; delete responseHeaders['content-length']; delete responseHeaders['transfer-encoding']; res.writeHead(proxyRes.statusCode, responseHeaders); res.end(responseBody); }); }); proxyReq.on('error', (error) => { console.error('[Error] Proxy request failed:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Proxy request failed', details: error.message })); }); proxyReq.write(JSON.stringify(cleanedBody)); proxyReq.end(); } catch (error) { console.error('[Error] Failed to parse request:', error); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid JSON', details: error.message })); } }); } // Start server const server = http.createServer(handleRequest); server.listen(PORT, '127.0.0.1', () => { console.log(`[Startup] Proxy server running on http://127.0.0.1:${PORT}`); console.log(`[Config] Target API: ${TARGET_API}`); }); ``` ### OpenCode Configuration **File: `opencode.json`** ```json { "$schema": "https://opencode.ai/config.json", "provider": { "proxy": { "npm": "@ai-sdk/openai-compatible", "name": "proxy", "options": { "baseURL": "http://127.0.0.1:8080/", "apiKey": "local-proxy" }, "models": { "gemini-3-pro-preview": { "name": "Gemini-3-Pro-Preview", "attachment": true, "limit": { "context": 1048576, "output": 65535 }, "modalities": { "input": ["text"], "output": ["text"] } } } } } } ``` ### Usage 1. **Start the proxy server:** ```bash node proxy-server.js ``` 2. **Start OpenCode:** ```bash opencode ``` 3. **Verify functionality:** - The proxy will log filtered fields - Function Calling should work correctly with Gemini models ### Key Features - ✅ **Recursive Schema Cleaning** - Deep traversal of JSON Schema - ✅ **Compression Handling** - Automatic detection and decompression - ✅ **SSL Certificate Bypass** - Works with self-signed certificates - ✅ **Detailed Logging** - Easy debugging and troubleshooting - ✅ **Transparent Proxy** - Works seamlessly with OpenCode ### Limitations - ❌ Requires maintaining a separate Node.js process - ❌ Adds slight latency due to proxy layer - ❌ Not suitable for production environments without additional security measures ------ **Note:** This workaround has been tested and verified to work with `api.v3.cm/v1/` and Gemini models with Function Calling enabled.
yindo added the discussion label 2026-02-16 18:09:16 -05:00
Author
Owner

@github-actions[bot] commented on GitHub (Jan 31, 2026):

This issue might be a duplicate of existing issues. Please check:

  • #11413: "question" tool schema causes "400 Bad Request" with Gemini/Vertex AI - This issue describes similar JSON Schema compatibility problems with Gemini models, though it specifically involves the built-in question tool rather than general schema fields.

Feel free to ignore if your specific use case with OpenAI-compatible proxies addresses a different aspect of the problem.

@github-actions[bot] commented on GitHub (Jan 31, 2026): This issue might be a duplicate of existing issues. Please check: - #11413: "question" tool schema causes "400 Bad Request" with Gemini/Vertex AI - This issue describes similar JSON Schema compatibility problems with Gemini models, though it specifically involves the built-in question tool rather than general schema fields. Feel free to ignore if your specific use case with OpenAI-compatible proxies addresses a different aspect of the problem.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#8147