Skip to content

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

Closed

Description

@JoelJeffery-ux

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.

Activity

  1. added
    discussionUsed for feature requests, proposals, ideas, etc. Open discussion
    on Jan 31, 2026
  2. github-actions commented on Jan 31, 2026

    @github-actions
    Contributor

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

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

  3. github-actions commented on Apr 2, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 90 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

discussionUsed for feature requests, proposals, ideas, etc. Open discussion

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions