-
Notifications
You must be signed in to change notification settings - Fork 127
feat: Add Gemini compatibility mode #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jutaz
wants to merge
3
commits into
jamubc:main
Choose a base branch
from
jutaz:bugfix/gemini-tool-use
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # Schema Compatibility | ||
|
|
||
| ## The Problem: Divergent Tool Schemas | ||
|
|
||
| Different Large Language Models can have different levels of strictness when it comes to validating the schemas of the tools they are provided. For example, Google Gemini's API enforces a stricter subset of the JSON Schema specification than other models might. | ||
|
|
||
| A common issue is the use of complex schema types like `anyOf` (which is what `zod` produces for a `z.union`). While valid JSON Schema, Gemini's API may reject it, causing `400 Bad Request` errors and preventing the tool from being called. | ||
|
|
||
| A simple solution would be to make all schemas conform to the strictest possible requirements, but this would reduce the expressiveness and validation power of our internal schemas for models that *do* support them. | ||
|
|
||
| ## The Solution: A Strategy-Based Approach | ||
|
|
||
| To solve this, we've implemented a **strategy-based pattern** that allows the server to dynamically select the appropriate schema for a tool parameter based on a startup configuration. This gives us the best of both worlds: strict, compatible schemas for models that need them, and rich, expressive schemas for those that don't. | ||
|
|
||
| The system is composed of two parts: | ||
|
|
||
| ### 1. Startup Configuration | ||
|
|
||
| The server's "mode" is determined once at startup by the `--target-model` command-line flag or the `MCP_TARGET_MODEL` environment variable. This is managed in `@/src/utils/config.ts`. | ||
|
|
||
| ```bash | ||
| # Start the server in Gemini compatibility mode | ||
| npx gemini-mcp-tool --target-model gemini | ||
| ``` | ||
|
|
||
| If no target is specified, it defaults to `'default'`. | ||
|
|
||
| ### 2. Schema Strategies | ||
|
|
||
| The core of the solution is in `@/src/utils/schema-strategies.ts`. This file contains functions (strategies) that are responsible for choosing which Zod schema to use based on the `config.target`. | ||
|
|
||
| This approach keeps the conditional logic centralized and allows our tool definitions to remain clean and declarative. | ||
|
|
||
| ## Developer Guide: Adding a Compatible Parameter | ||
|
|
||
| If you are adding a new tool parameter that requires a different schema for a specific target (like Gemini), follow this pattern. We will use the `chunkIndex` parameter in the `ask-gemini` tool as our example. | ||
|
|
||
| ### Step 1: Define Both Schema Variations | ||
|
|
||
| In your tool definition file (e.g., `@/src/tools/ask-gemini.tool.ts`), define both the standard and the model-specific schemas as named constants. | ||
|
|
||
| The `gemini` version should be the simplest possible schema that the API will accept. It's also a good practice to include a `z.preprocess` step to gracefully handle any data that might still come in the "standard" format (e.g., a number) before validation. | ||
|
|
||
| ```typescript | ||
| // @/src/tools/ask-gemini.tool.ts | ||
|
|
||
| // The standard, expressive schema for most models | ||
| const standardChunkIndexSchema = z.union([z.number(), z.string()]).optional().describe("Which chunk to return (1-based)"); | ||
|
|
||
| // The simplified schema for Gemini's stricter API | ||
| const geminiChunkIndexSchema = z.preprocess( | ||
| (val) => (val === undefined || val === null ? val : String(val)), | ||
| z.string().optional() | ||
| ).describe("Which chunk to return (1-based)"); | ||
| ``` | ||
|
|
||
| ### Step 2: Create or Update a Strategy | ||
|
|
||
| In `@/src/utils/schema-strategies.ts`, ensure there is a strategy function that can select the correct schema. If you're adding a parameter with a new compatibility need, you may need to add a new strategy function. | ||
|
|
||
| Our existing strategy for `chunkIndex` looks like this: | ||
|
|
||
| ```typescript | ||
| // @/src/utils/schema-strategies.ts | ||
|
|
||
| export const selectChunkIndexSchema = (schemas: { | ||
| standard: ZodTypeAny; | ||
| gemini: ZodTypeAny; | ||
| }): ZodTypeAny => { | ||
| if (config.target === 'gemini') { | ||
| return schemas.gemini; | ||
| } | ||
| return schemas.standard; | ||
| }; | ||
| ``` | ||
|
|
||
| ### Step 3: Use the Strategy in Your Tool Definition | ||
|
|
||
| Finally, in your tool's main Zod schema, import and use the strategy function. Pass your previously defined schemas to it. This makes your definition declarative and clean. | ||
|
|
||
| ```typescript | ||
| // @/src/tools/ask-gemini.tool.ts | ||
| import { selectChunkIndexSchema } from '../utils/schema-strategies.js'; | ||
|
|
||
| // ... (schemas defined above) ... | ||
|
|
||
| const askGeminiArgsSchema = z.object({ | ||
| // ... other parameters | ||
| chunkIndex: selectChunkIndexSchema({ | ||
| standard: standardChunkIndexSchema, | ||
| gemini: geminiChunkIndexSchema, | ||
| }), | ||
| // ... other parameters | ||
| }); | ||
| ``` | ||
|
|
||
| By following this pattern, you can easily support multiple model targets without cluttering your tool definitions with conditional logic. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /** | ||
| * @module config | ||
| * Manages the server's runtime configuration, primarily for determining the | ||
| * target LLM environment. This allows the application to adapt its behavior, | ||
| * such as tool schema generation, based on the specific model it's serving. | ||
| * | ||
| * The configuration is determined once at startup by checking command-line | ||
| * arguments and environment variables, then exported as a read-only object. | ||
| */ | ||
| import { Logger } from './logger.js'; | ||
|
|
||
| /** | ||
| * An immutable, frozen object containing the server's runtime configuration. | ||
| */ | ||
| export interface AppConfig { | ||
| /** The target model environment, used for feature switching like schema generation. */ | ||
| readonly target: string; | ||
| } | ||
|
|
||
| /** | ||
| * Initializes the application configuration by reading from the environment. | ||
| * This function should only be executed once when the application starts. | ||
| * | ||
| * The configuration is resolved with the following precedence: | ||
| * 1. Command-line argument: `--target-model <value>` | ||
| * 2. Environment variable: `MCP_TARGET_MODEL=<value>` | ||
| * 3. Default value (`'default'`) | ||
| * | ||
| * @returns A frozen `AppConfig` object. | ||
| */ | ||
| function initializeConfig(): AppConfig { | ||
| let target = 'default'; | ||
| let detectedVia = ''; | ||
|
|
||
| // 1. Check for the command-line argument | ||
| const argIndex = process.argv.indexOf('--target-model'); | ||
| if (argIndex > -1 && process.argv.length > argIndex + 1) { | ||
| target = process.argv[argIndex + 1].toLowerCase(); | ||
| detectedVia = 'command-line argument'; | ||
| } | ||
| // 2. Check for the environment variable if not already set by arg | ||
| else if (process.env.MCP_TARGET_MODEL) { | ||
| target = process.env.MCP_TARGET_MODEL.toLowerCase(); | ||
| detectedVia = 'environment variable'; | ||
| } | ||
|
|
||
| if (target !== 'default') { | ||
| Logger.debug(`Target model set to "${target}" via ${detectedVia}.`); | ||
| } | ||
|
|
||
| // Return a frozen, read-only configuration object | ||
| return Object.freeze({ target }); | ||
| } | ||
|
|
||
| /** | ||
| * The single, application-wide configuration instance. | ||
| * Initialized once at startup. | ||
| */ | ||
| export const config: AppConfig = initializeConfig(); | ||
|
|
||
| /** | ||
| * A utility function to quickly check if the Gemini compatibility mode is active. | ||
| * This is the preferred way to check for the target in application logic. | ||
| * | ||
| * @returns `true` if the target model is Gemini, otherwise `false`. | ||
| */ | ||
| export function isGeminiTarget(): boolean { | ||
| return config.target === 'gemini'; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.