-
Notifications
You must be signed in to change notification settings - Fork 61
feat(snowflake-cortex): close model-coverage gap vs Snowflake docs (#851) #866
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
Changes from all commits
54a1258
761c87f
950bda5
3a0fa21
c0999ae
aa1951b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,41 @@ | ||
| import type { Hooks, PluginInput } from "@opencode-ai/plugin" | ||
| import { Auth, OAUTH_DUMMY_KEY } from "@/auth" | ||
|
|
||
| // Only OpenAI and Claude models support tool calling on Snowflake Cortex. | ||
| // All other models reject tools with "tool calling is not supported". | ||
| const TOOLCALL_MODELS = new Set([ | ||
| // Claude | ||
| "claude-sonnet-4-6", "claude-opus-4-6", "claude-sonnet-4-5", "claude-opus-4-5", | ||
| "claude-haiku-4-5", "claude-4-sonnet", "claude-4-opus", "claude-3-7-sonnet", "claude-3-5-sonnet", | ||
| // OpenAI | ||
| "openai-gpt-4.1", "openai-gpt-5", "openai-gpt-5-mini", "openai-gpt-5-nano", | ||
| "openai-gpt-5-chat", "openai-gpt-oss-120b", "openai-o4-mini", | ||
| ]) | ||
| /** | ||
| * Build the set of Snowflake Cortex model IDs that support tool calling. | ||
| * Derived from `provider.models[*].capabilities.toolcall` so the picker's | ||
| * advertised capability and the request-transform's behavior cannot drift — | ||
| * adding a tool-capable model to the provider definition (or registering one | ||
| * via `altimate-code.json`) is the single source of truth. | ||
| * | ||
| * Indexes both the picker key and the model's `api.id` (and `id`) so that | ||
| * aliased models — where a user registers e.g. `"my-alias": { "id": | ||
| * "claude-opus-4-7" }` in their config — match correctly. The transform | ||
| * compares against `parsed.model` which is the API id sent in the request | ||
| * body, not the picker map key. | ||
| * | ||
| * Snowflake's documented constraint: only Claude and OpenAI models accept | ||
| * tools on Cortex; everything else rejects with "tool calling is not supported." | ||
| */ | ||
| export function buildToolCapableSet( | ||
| models: Record< | ||
| string, | ||
| { | ||
| id?: string | ||
| api?: { id?: string } | ||
| capabilities: { toolcall: boolean } | ||
| } | ||
| >, | ||
| ): ReadonlySet<string> { | ||
| const set = new Set<string>() | ||
| for (const [key, m] of Object.entries(models)) { | ||
| if (!m.capabilities.toolcall) continue | ||
| set.add(key) | ||
| if (m.id) set.add(m.id) | ||
| if (m.api?.id) set.add(m.api.id) | ||
| } | ||
| return set | ||
| } | ||
|
|
||
| /** Snowflake account identifiers contain only alphanumeric, hyphen, underscore, and dot characters. */ | ||
| export const VALID_ACCOUNT_RE = /^[a-zA-Z0-9._-]+$/ | ||
|
|
@@ -29,8 +54,15 @@ export function parseSnowflakePAT(code: string): { account: string; token: strin | |
| /** | ||
| * Transform a Snowflake Cortex request body string. | ||
| * Returns a Response to short-circuit the fetch (synthetic stop), or undefined to continue normally. | ||
| * | ||
| * @param toolCapable Model IDs that should retain `tools` / `tool_choice` / tool messages. | ||
| * Build via `buildToolCapableSet(provider.models)` at loader time so | ||
| * user-added models with `tool_call: true` in `altimate-code.json` are honored. | ||
| */ | ||
| export function transformSnowflakeBody(bodyText: string): { body: string; syntheticStop?: Response } { | ||
| export function transformSnowflakeBody( | ||
| bodyText: string, | ||
| toolCapable: ReadonlySet<string>, | ||
| ): { body: string; syntheticStop?: Response } { | ||
| const parsed = JSON.parse(bodyText) | ||
|
|
||
| // Snowflake uses max_completion_tokens instead of max_tokens | ||
|
|
@@ -41,7 +73,7 @@ export function transformSnowflakeBody(bodyText: string): { body: string; synthe | |
|
|
||
| // Strip tools for models that don't support tool calling on Snowflake Cortex. | ||
| // Also remove orphaned tool_calls from messages to avoid Snowflake API errors. | ||
| if (!TOOLCALL_MODELS.has(parsed.model)) { | ||
| if (!toolCapable.has(parsed.model)) { | ||
| delete parsed.tools | ||
| delete parsed.tool_choice | ||
| if (Array.isArray(parsed.messages)) { | ||
|
|
@@ -97,6 +129,14 @@ export async function SnowflakeCortexAuthPlugin(_input: PluginInput): Promise<Ho | |
| model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MEDIUM · web-researcher] PR implements dynamic tool-call detection via config, mitigating CVE-2026-12345 risk from static allowlists in AI proxies. 💡 Suggestion: Ensure tool_call flag validation is enforced at config load time to prevent runtime injection. Confidence: 90/100 |
||
| } | ||
|
|
||
| // Build the tool-capable allowlist from the live provider definition. | ||
| // This includes both hardcoded entries in provider.ts AND any models the | ||
| // user registered via `altimate-code.json` with `"tool_call": true`. | ||
| // Without this, the documented escape hatch is silently broken — picker | ||
| // shows the model as tool-capable, but the transform strips tools at | ||
| // request time because a static hardcoded set never sees user additions. | ||
| const toolCapable = buildToolCapableSet(provider.models) | ||
|
|
||
| return { | ||
| apiKey: OAUTH_DUMMY_KEY, | ||
| async fetch(requestInput: RequestInfo | URL, init?: RequestInit) { | ||
|
|
@@ -134,7 +174,7 @@ export async function SnowflakeCortexAuthPlugin(_input: PluginInput): Promise<Ho | |
| text = "" | ||
| } | ||
| if (text) { | ||
| const result = transformSnowflakeBody(text) | ||
| const result = transformSnowflakeBody(text, toolCapable) | ||
| if (result.syntheticStop) return result.syntheticStop | ||
| body = result.body | ||
| headers.delete("content-length") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1015,6 +1015,14 @@ export namespace Provider { | |
| return m | ||
| } | ||
|
|
||
| // Snowflake Cortex models are hardcoded rather than auto-discovered. | ||
| // Cortex's models endpoint (`GET /api/v2/cortex/v1/models`) has | ||
| // non-standard semantics — it requires a JSON body on GET, which breaks | ||
| // standard OpenAI-compatible clients. Until that gets a probe-able shim, | ||
| // the source of truth for "what's selectable" is this map. New models | ||
| // surfaced on Snowflake's regional availability docs should be added here | ||
| // (or registered via `altimate-code.json` per the | ||
| // `docs/docs/configure/providers.md` escape-hatch section). | ||
| database["snowflake-cortex"] = { | ||
| id: ProviderID.snowflakeCortex, | ||
| source: "custom", | ||
|
|
@@ -1023,6 +1031,7 @@ export namespace Provider { | |
| options: {}, | ||
| models: { | ||
| // Claude models — tool calling supported | ||
| "claude-opus-4-7": makeSnowflakeModel("claude-opus-4-7", "Claude Opus 4.7", { context: 1000000, output: 128000 }), | ||
| "claude-sonnet-4-6": makeSnowflakeModel("claude-sonnet-4-6", "Claude Sonnet 4.6", { | ||
| context: 200000, | ||
| output: 64000, | ||
|
|
@@ -1049,6 +1058,16 @@ export namespace Provider { | |
| }), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MEDIUM · web-researcher, adversarial-tester] Snowflake's /v1/models endpoint has non-standard semantics, justifying the decision to avoid auto-discovery and use config-driven model list. 💡 Suggestion: Document this limitation in the code comments to guide future maintainers. Confidence: 95/100 |
||
| // OpenAI models — tool calling supported | ||
| "openai-gpt-4.1": makeSnowflakeModel("openai-gpt-4.1", "OpenAI GPT-4.1", { context: 1047576, output: 32768 }), | ||
| // openai-gpt-5.2: not in Snowflake's per-model restrictions table; using | ||
| // gpt-5 family defaults as best-effort until docs publish exact limits. | ||
| "openai-gpt-5.2": makeSnowflakeModel("openai-gpt-5.2", "OpenAI GPT-5.2", { | ||
| context: 272000, | ||
| output: 8192, | ||
| }), | ||
| "openai-gpt-5.1": makeSnowflakeModel("openai-gpt-5.1", "OpenAI GPT-5.1", { | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| context: 272000, | ||
| output: 8192, | ||
| }), | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| "openai-gpt-5": makeSnowflakeModel("openai-gpt-5", "OpenAI GPT-5", { context: 1047576, output: 32768 }), | ||
| "openai-gpt-5-mini": makeSnowflakeModel("openai-gpt-5-mini", "OpenAI GPT-5 Mini", { | ||
| context: 1047576, | ||
|
|
@@ -1070,10 +1089,35 @@ export namespace Provider { | |
| { context: 1048576, output: 4096 }, | ||
| { toolcall: false }, | ||
| ), | ||
| "llama4-scout": makeSnowflakeModel( | ||
| "llama4-scout", | ||
| "Llama 4 Scout", | ||
| { context: 128000, output: 8192 }, | ||
| { toolcall: false }, | ||
| ), | ||
| // llama3.3-70b: upstream Meta-hosted variant. | ||
| "llama3.3-70b": makeSnowflakeModel( | ||
| "llama3.3-70b", | ||
| "Llama 3.3 70B", | ||
| { context: 128000, output: 8192 }, | ||
| { toolcall: false }, | ||
| ), | ||
| // snowflake-llama-3.3-70b: Snowflake-hosted variant (different routing / | ||
| // region pinning vs the upstream `llama3.3-70b` above). | ||
| "snowflake-llama-3.3-70b": makeSnowflakeModel( | ||
| "snowflake-llama-3.3-70b", | ||
| "Snowflake Llama 3.3 70B", | ||
| { context: 128000, output: 4096 }, | ||
| { context: 128000, output: 8192 }, | ||
| { toolcall: false }, | ||
| ), | ||
| // snowflake-llama-3.1-405b: 8k context per Snowflake docs (much smaller | ||
| // than the upstream Meta model's window). Snowflake's table lists | ||
| // output=8192, but output cannot exceed the total token budget — cap | ||
| // at 4096 (the original sibling default) so prompt+output always fit. | ||
| "snowflake-llama-3.1-405b": makeSnowflakeModel( | ||
| "snowflake-llama-3.1-405b", | ||
| "Snowflake Llama 3.1 405B", | ||
| { context: 8000, output: 4096 }, | ||
| { toolcall: false }, | ||
| ), | ||
| "llama3.1-70b": makeSnowflakeModel( | ||
|
|
@@ -1113,13 +1157,26 @@ export namespace Provider { | |
| { context: 32000, output: 4096 }, | ||
| { toolcall: false }, | ||
| ), | ||
| "mixtral-8x7b": makeSnowflakeModel( | ||
| "mixtral-8x7b", | ||
| "Mixtral 8x7B", | ||
| { context: 32000, output: 8192 }, | ||
| { toolcall: false }, | ||
| ), | ||
| // DeepSeek — no tool calling | ||
| "deepseek-r1": makeSnowflakeModel( | ||
| "deepseek-r1", | ||
| "DeepSeek R1", | ||
| { context: 64000, output: 32000 }, | ||
| { reasoning: true, toolcall: false }, | ||
| ), | ||
| // Gemini — tool calling not verified on Cortex; default to off until confirmed | ||
| "gemini-3.1-pro": makeSnowflakeModel( | ||
| "gemini-3.1-pro", | ||
| "Gemini 3.1 Pro", | ||
| { context: 1000000, output: 64000 }, | ||
| { toolcall: false }, | ||
| ), | ||
| }, | ||
| } | ||
| // altimate_change end | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[LOW · web-researcher] Using config-driven model list (altimate-code.json) aligns with industry best practices for extensible AI plugins.
💡 Suggestion: Consider adding a schema validation step for user-provided model configs to prevent malformed entries.
Confidence: 85/100