From 54a1258e76507507f906a014234dc60c3d60b28c Mon Sep 17 00:00:00 2001 From: Haider Date: Mon, 1 Jun 2026 16:41:25 +0530 Subject: [PATCH 1/5] feat(snowflake-cortex): close model-coverage gap vs Snowflake docs (#851) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #851. altimate-code hardcodes the Snowflake Cortex model list in `provider.ts` (it doesn't auto-discover via `/v1/models` — Snowflake's endpoint has non-standard semantics that break standard clients). The list had drifted behind Snowflake's regional availability matrix, so several models that work on Cortex couldn't be selected from the picker — most visibly `claude-opus-4-7`, which prompted the report. Adds the 8 missing models per Snowflake's current regional availability matrix: - Claude: `claude-opus-4-7` (toolcall: true) - OpenAI: `openai-gpt-5.1`, `openai-gpt-5.2` (toolcall: true) - Llama: `llama4-scout`, `llama3.3-70b`, `snowflake-llama-3.1-405b` (toolcall: false) - Mistral: `mixtral-8x7b` (toolcall: false) - Gemini: `gemini-3.1-pro` (toolcall: false — tool calling unverified on Cortex; conservative default per the rule that only Claude+OpenAI reliably support tools today) Also updates `TOOLCALL_MODELS` in `snowflake.ts` to include the new Claude and OpenAI entries so request transform doesn't strip tools. Documents the config-extension escape hatch for the next drift cycle: users can add a model under `provider['snowflake-cortex'].models` in `opencode.json` and it merges into the built-in list (mechanism wired at provider.ts:1320-1399). New section in `docs/docs/configure/providers.md` shows the pattern so users don't need to fork or wait for a release when Snowflake adds a model. Tests: - `models added per Snowflake regional availability docs (issue #851)` asserts every newly-added model is defined with the right toolcall capability. - `user can register a model not in the hardcoded list via opencode.json` exercises the config-extension path end-to-end. 50 → 52 tests pass in `test/provider/snowflake.test.ts`; typecheck clean. Longer-term, true auto-discovery (a Snowflake-specific shim around `POST /api/v2/cortex/v1/models` or the GET-with-body variant) would eliminate drift entirely — flagged on the issue for maintainer input. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/docs/configure/providers.md | 32 +++++-- .../opencode/src/altimate/plugin/snowflake.ts | 4 +- packages/opencode/src/provider/provider.ts | 40 +++++++++ .../opencode/test/provider/snowflake.test.ts | 89 +++++++++++++++++++ 4 files changed, 158 insertions(+), 7 deletions(-) diff --git a/docs/docs/configure/providers.md b/docs/docs/configure/providers.md index 6c13590cac..0a84a53680 100644 --- a/docs/docs/configure/providers.md +++ b/docs/docs/configure/providers.md @@ -280,15 +280,37 @@ Billing flows through your Snowflake credits — no per-token costs. | Model | Tool Calling | |-------|-------------| -| `claude-sonnet-4-6`, `claude-opus-4-6`, `claude-sonnet-4-5`, `claude-opus-4-5`, `claude-haiku-4-5`, `claude-4-sonnet`, `claude-3-7-sonnet`, `claude-3-5-sonnet` | Yes | -| `openai-gpt-4.1`, `openai-gpt-5`, `openai-gpt-5-mini`, `openai-gpt-5-nano`, `openai-gpt-5-chat` | Yes | -| `llama4-maverick`, `snowflake-llama-3.3-70b`, `llama3.1-70b`, `llama3.1-405b`, `llama3.1-8b` | No | -| `mistral-large`, `mistral-large2`, `mistral-7b` | No | -| `deepseek-r1` | No | +| `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-opus-4-6`, `claude-sonnet-4-5`, `claude-opus-4-5`, `claude-haiku-4-5`, `claude-4-sonnet`, `claude-3-7-sonnet`, `claude-3-5-sonnet` | Yes | +| `openai-gpt-4.1`, `openai-gpt-5`, `openai-gpt-5.1`, `openai-gpt-5.2`, `openai-gpt-5-mini`, `openai-gpt-5-nano`, `openai-gpt-5-chat` | Yes | +| `llama4-maverick`, `llama4-scout`, `llama3.3-70b`, `snowflake-llama-3.3-70b`, `snowflake-llama-3.1-405b`, `llama3.1-70b`, `llama3.1-405b`, `llama3.1-8b` | No | +| `mistral-large`, `mistral-large2`, `mistral-7b`, `mixtral-8x7b` | No | +| `deepseek-r1`, `gemini-3.1-pro` | No | !!! note Model availability depends on your Snowflake region. Enable cross-region inference with `ALTER ACCOUNT SET CORTEX_ENABLED_CROSS_REGION = 'ANY_REGION'` for full model access. +### Adding a model not in the list + +Snowflake Cortex adds models faster than this list can be updated. If a model is available on your Cortex account but not yet listed above, you can register it locally without forking the CLI — add it under `provider["snowflake-cortex"].models` in your `opencode.json` (or `.opencode/opencode.json`): + +```json +{ + "provider": { + "snowflake-cortex": { + "models": { + "your-new-model-id": { + "name": "Your New Model", + "limit": { "context": 200000, "output": 32000 }, + "tool_call": true + } + } + } + } +} +``` + +The entry merges with the built-in list, so the model appears in the picker and can be selected as `snowflake-cortex/your-new-model-id`. Set `"tool_call": false` for models that don't support tools on Cortex (Llama, Mistral, DeepSeek, Gemini today) — otherwise requests with tools will fail. + ## Databricks AI Gateway Connect to Databricks serving endpoints (Foundation Model APIs) via your workspace PAT. Use Databricks-hosted Llama, Claude, GPT, Gemini, DBRX, or Mixtral for agent reasoning — billing flows through your Databricks account. diff --git a/packages/opencode/src/altimate/plugin/snowflake.ts b/packages/opencode/src/altimate/plugin/snowflake.ts index 79a6bb112b..ead90aa8bb 100644 --- a/packages/opencode/src/altimate/plugin/snowflake.ts +++ b/packages/opencode/src/altimate/plugin/snowflake.ts @@ -5,10 +5,10 @@ import { Auth, OAUTH_DUMMY_KEY } from "@/auth" // 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-opus-4-7", "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-4.1", "openai-gpt-5", "openai-gpt-5.1", "openai-gpt-5.2", "openai-gpt-5-mini", "openai-gpt-5-nano", "openai-gpt-5-chat", "openai-gpt-oss-120b", "openai-o4-mini", ]) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 5f5b9f0bf9..39acd0e5a7 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1023,6 +1023,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: 200000, output: 32000 }), "claude-sonnet-4-6": makeSnowflakeModel("claude-sonnet-4-6", "Claude Sonnet 4.6", { context: 200000, output: 64000, @@ -1049,6 +1050,14 @@ export namespace Provider { }), // 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": makeSnowflakeModel("openai-gpt-5.2", "OpenAI GPT-5.2", { + context: 1047576, + output: 32768, + }), + "openai-gpt-5.1": makeSnowflakeModel("openai-gpt-5.1", "OpenAI GPT-5.1", { + context: 1047576, + output: 32768, + }), "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,12 +1079,30 @@ export namespace Provider { { context: 1048576, output: 4096 }, { toolcall: false }, ), + "llama4-scout": makeSnowflakeModel( + "llama4-scout", + "Llama 4 Scout", + { context: 1048576, output: 4096 }, + { toolcall: false }, + ), + "llama3.3-70b": makeSnowflakeModel( + "llama3.3-70b", + "Llama 3.3 70B", + { context: 128000, output: 4096 }, + { toolcall: false }, + ), "snowflake-llama-3.3-70b": makeSnowflakeModel( "snowflake-llama-3.3-70b", "Snowflake Llama 3.3 70B", { context: 128000, output: 4096 }, { toolcall: false }, ), + "snowflake-llama-3.1-405b": makeSnowflakeModel( + "snowflake-llama-3.1-405b", + "Snowflake Llama 3.1 405B", + { context: 128000, output: 4096 }, + { toolcall: false }, + ), "llama3.1-70b": makeSnowflakeModel( "llama3.1-70b", "Llama 3.1 70B", @@ -1113,6 +1140,12 @@ export namespace Provider { { context: 32000, output: 4096 }, { toolcall: false }, ), + "mixtral-8x7b": makeSnowflakeModel( + "mixtral-8x7b", + "Mixtral 8x7B", + { context: 32000, output: 4096 }, + { toolcall: false }, + ), // DeepSeek — no tool calling "deepseek-r1": makeSnowflakeModel( "deepseek-r1", @@ -1120,6 +1153,13 @@ export namespace Provider { { 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: 1048576, output: 8192 }, + { toolcall: false }, + ), }, } // altimate_change end diff --git a/packages/opencode/test/provider/snowflake.test.ts b/packages/opencode/test/provider/snowflake.test.ts index f4e20db74e..762625552d 100644 --- a/packages/opencode/test/provider/snowflake.test.ts +++ b/packages/opencode/test/provider/snowflake.test.ts @@ -586,6 +586,95 @@ describe("snowflake-cortex provider", () => { } }) + test("models added per Snowflake regional availability docs (issue #851)", async () => { + // Regression: PR for issue #851 added 8 models that Snowflake Cortex + // supports but were missing from the hardcoded list. Lock them in so a + // future refactor doesn't silently drop them. + await setupOAuth() + try { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://altimate.ai/config.json" })) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const models = providers["snowflake-cortex"].models + // Claude (toolcall: true) + expect(models["claude-opus-4-7"]).toBeDefined() + expect(models["claude-opus-4-7"].capabilities.toolcall).toBe(true) + // OpenAI (toolcall: true) + expect(models["openai-gpt-5.1"]).toBeDefined() + expect(models["openai-gpt-5.1"].capabilities.toolcall).toBe(true) + expect(models["openai-gpt-5.2"]).toBeDefined() + expect(models["openai-gpt-5.2"].capabilities.toolcall).toBe(true) + // Llama (toolcall: false) + expect(models["llama4-scout"]).toBeDefined() + expect(models["llama4-scout"].capabilities.toolcall).toBe(false) + expect(models["llama3.3-70b"]).toBeDefined() + expect(models["llama3.3-70b"].capabilities.toolcall).toBe(false) + expect(models["snowflake-llama-3.1-405b"]).toBeDefined() + expect(models["snowflake-llama-3.1-405b"].capabilities.toolcall).toBe(false) + // Mixtral (toolcall: false) + expect(models["mixtral-8x7b"]).toBeDefined() + expect(models["mixtral-8x7b"].capabilities.toolcall).toBe(false) + // Gemini (toolcall: false — unverified on Cortex) + expect(models["gemini-3.1-pro"]).toBeDefined() + expect(models["gemini-3.1-pro"].capabilities.toolcall).toBe(false) + }, + }) + } finally { + await restoreAuth() + } + }) + + test("user can register a model not in the hardcoded list via opencode.json", async () => { + // Documents the option (2) escape hatch: when Snowflake adds a model + // before the CLI's hardcoded list catches up, users add it under + // provider['snowflake-cortex'].models and it merges into the picker. + await setupOAuth() + try { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://altimate.ai/config.json", + provider: { + "snowflake-cortex": { + models: { + "future-model-x": { + name: "Future Model X", + limit: { context: 200000, output: 32000 }, + tool_call: true, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const m = providers["snowflake-cortex"].models["future-model-x"] + expect(m).toBeDefined() + expect(m.name).toBe("Future Model X") + expect(m.capabilities.toolcall).toBe(true) + expect(m.limit.context).toBe(200000) + // Built-in models still present alongside the config-added one. + expect(providers["snowflake-cortex"].models["claude-opus-4-7"]).toBeDefined() + }, + }) + } finally { + await restoreAuth() + } + }) + test("claude-3-5-sonnet output limit is 8192", async () => { await setupOAuth() try { From 761c87f3ed7bf1496bb8d3f2c97fec58811fc207 Mon Sep 17 00:00:00 2001 From: Haider Date: Mon, 1 Jun 2026 17:22:26 +0530 Subject: [PATCH 2/5] =?UTF-8?q?fix(snowflake-cortex):=20consensus=20review?= =?UTF-8?q?=20fixes=20=E2=80=94=20limits=20+=20escape=20hatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the consensus code review on PR #866. Two critical bugs found by independent verification against Snowflake's regional availability docs (https://docs.snowflake.com/en/user-guide/snowflake-cortex/aisql-regional-availability): 1. **Correct context/output limits on every newly-added model.** The initial commit inferred limits from sibling models; cross-check against the doc showed most were wrong, and `snowflake-llama-3.1-405b` was off by 16× on context (would silently fail any prompt above 8k tokens). Updated values: - `claude-opus-4-7`: 200000/32000 → 1000000/128000 - `openai-gpt-5.1`: 1047576/32768 → 272000/8192 - `openai-gpt-5.2`: 1047576/32768 → 272000/8192 (not in restrictions table; using gpt-5 family defaults with a comment) - `llama4-scout`: 1048576/4096 → 128000/8192 - `llama3.3-70b`: 128000/4096 → 128000/8192 - `snowflake-llama-3.1-405b`: 128000/4096 → 8000/8192 - `mixtral-8x7b`: 32000/4096 → 32000/8192 - `gemini-3.1-pro`: 1048576/8192 → 1000000/64000 2. **Fix the documented opencode.json escape hatch for tool-capable user-added models.** The `TOOLCALL_MODELS` hardcoded set in `snowflake.ts` was a second source of truth that the picker capability had to be kept in sync with. Worse, the docs change in the first commit told users they could mark a custom model `"tool_call": true` — but the request transform consulted only the static set, so user-added models had `tools` silently stripped at request time even though the picker advertised them as tool-capable. Replaced the hardcoded set with `buildToolCapableSet(models)` — the loader walks `provider.models` (which already includes any models the user registered via `opencode.json`) and derives the allowlist from `capabilities.toolcall`. Single source of truth; the picker and the transform can't drift; the escape hatch works. `transformSnowflakeBody` now takes an explicit allowlist parameter; the loader passes the dynamically-built set. Existing call sites updated to pass a shared `TOOLCAPABLE_FIXTURE`. Also folded into this commit: - Comments distinguishing `llama3.3-70b` (upstream Meta) from `snowflake-llama-3.3-70b` (Snowflake-hosted variant). - Doc note clarifying the `tool_call` snake_case field maps to `capabilities.toolcall`. - The "models added per Snowflake regional availability docs (issue #851)" regression test now asserts `limit.context` and `limit.output` per model in addition to identity and toolcall capability. - New tests: - `buildToolCapableSet derives the allowlist from provider model capabilities` — parity between picker and transform. - `escape-hatch model with tool_call: true retains tools through transformSnowflakeBody` — end-to-end check the escape hatch actually works for tool-capable user models. - `escape-hatch model with tool_call: false has tools stripped through transformSnowflakeBody` — the inverse, mirroring the built-in Llama/Mistral behavior for user-registered non-tool models. Deferred (per maintainer guidance pending @anandgupta42's input on the long-term auto-discovery question): - Removing the legacy `claude-3-7-sonnet`, `claude-3-5-sonnet`, `openai-gpt-5-chat` entries — kept per the original PR description. - Auto-discovery shim around Snowflake's `/api/v2/cortex/v1/models` — out of scope until maintainer green. Tests: typecheck clean across all 5 packages; 55/55 in `test/provider/snowflake.test.ts` (was 52, +3 net for new coverage). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/docs/configure/providers.md | 2 + .../opencode/src/altimate/plugin/snowflake.ts | 50 +++- packages/opencode/src/provider/provider.ts | 29 ++- .../altimate/cortex-snowflake-e2e.test.ts | 15 +- .../opencode/test/provider/snowflake.test.ts | 245 ++++++++++++++---- 5 files changed, 264 insertions(+), 77 deletions(-) diff --git a/docs/docs/configure/providers.md b/docs/docs/configure/providers.md index 0a84a53680..d987ec659b 100644 --- a/docs/docs/configure/providers.md +++ b/docs/docs/configure/providers.md @@ -311,6 +311,8 @@ Snowflake Cortex adds models faster than this list can be updated. If a model is The entry merges with the built-in list, so the model appears in the picker and can be selected as `snowflake-cortex/your-new-model-id`. Set `"tool_call": false` for models that don't support tools on Cortex (Llama, Mistral, DeepSeek, Gemini today) — otherwise requests with tools will fail. +The `tool_call` field uses snake_case (matching the rest of the `opencode.json` schema) and maps to the picker's `capabilities.toolcall`. The request transform reads the same value, so a user-added model marked `tool_call: true` keeps `tools` and `tool_choice` in outgoing requests — and one marked `tool_call: false` has them stripped, the same as the built-in non-tool entries. + ## Databricks AI Gateway Connect to Databricks serving endpoints (Foundation Model APIs) via your workspace PAT. Use Databricks-hosted Llama, Claude, GPT, Gemini, DBRX, or Mixtral for agent reasoning — billing flows through your Databricks account. diff --git a/packages/opencode/src/altimate/plugin/snowflake.ts b/packages/opencode/src/altimate/plugin/snowflake.ts index ead90aa8bb..d2582c288c 100644 --- a/packages/opencode/src/altimate/plugin/snowflake.ts +++ b/packages/opencode/src/altimate/plugin/snowflake.ts @@ -1,16 +1,25 @@ 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-opus-4-7", "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.1", "openai-gpt-5.2", "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 `opencode.json`) is the single source of truth. + * + * 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, +): ReadonlySet { + const set = new Set() + for (const [id, m] of Object.entries(models)) { + if (m.capabilities.toolcall) set.add(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 +38,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 `opencode.json` are honored. */ -export function transformSnowflakeBody(bodyText: string): { body: string; syntheticStop?: Response } { +export function transformSnowflakeBody( + bodyText: string, + toolCapable: ReadonlySet, +): { body: string; syntheticStop?: Response } { const parsed = JSON.parse(bodyText) // Snowflake uses max_completion_tokens instead of max_tokens @@ -41,7 +57,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 +113,14 @@ export async function SnowflakeCortexAuthPlugin(_input: PluginInput): Promise = new Set([ + "claude-3-5-sonnet", "claude-sonnet-4-6", "claude-opus-4-7", + "openai-gpt-4.1", "openai-gpt-5", +]) + // --------------------------------------------------------------------------- // Auth configuration // --------------------------------------------------------------------------- @@ -375,7 +382,7 @@ describe.skipIf(!HAS_CORTEX)("Snowflake Cortex E2E", () => { max_tokens: 100, stream: true, }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_E2E_FIXTURE) const parsed = JSON.parse(body) expect(parsed.max_completion_tokens).toBe(100) expect(parsed.max_tokens).toBeUndefined() @@ -388,7 +395,7 @@ describe.skipIf(!HAS_CORTEX)("Snowflake Cortex E2E", () => { tools: [{ type: "function", function: { name: "read_file" } }], tool_choice: "auto", }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_E2E_FIXTURE) const parsed = JSON.parse(body) expect(parsed.tools).toBeUndefined() expect(parsed.tool_choice).toBeUndefined() @@ -403,7 +410,7 @@ describe.skipIf(!HAS_CORTEX)("Snowflake Cortex E2E", () => { { role: "assistant", content: "response" }, ], }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_E2E_FIXTURE) expect(syntheticStop).toBeUndefined() }) @@ -416,7 +423,7 @@ describe.skipIf(!HAS_CORTEX)("Snowflake Cortex E2E", () => { { role: "assistant", content: "response" }, ], }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_E2E_FIXTURE) expect(syntheticStop).toBeDefined() expect(syntheticStop!.status).toBe(200) }) diff --git a/packages/opencode/test/provider/snowflake.test.ts b/packages/opencode/test/provider/snowflake.test.ts index 762625552d..0c23593041 100644 --- a/packages/opencode/test/provider/snowflake.test.ts +++ b/packages/opencode/test/provider/snowflake.test.ts @@ -5,7 +5,19 @@ import { Instance } from "../../src/project/instance" import { Provider } from "../../src/provider/provider" import { Auth } from "../../src/auth" import { Env } from "../../src/env" -import { parseSnowflakePAT, transformSnowflakeBody } from "../../src/altimate/plugin/snowflake" +import { buildToolCapableSet, parseSnowflakePAT, transformSnowflakeBody } from "../../src/altimate/plugin/snowflake" + +// Fixture allowlist for transformSnowflakeBody unit tests. Reflects what +// Snowflake Cortex actually accepts tools for today (Claude + OpenAI families). +// Production code derives the equivalent set from `provider.models` at loader +// time; this fixture exists so unit tests of the pure transform stay simple. +const TOOLCAPABLE_FIXTURE: ReadonlySet = new Set([ + "claude-opus-4-7", "claude-sonnet-4-6", "claude-opus-4-6", "claude-sonnet-4-5", + "claude-opus-4-5", "claude-haiku-4-5", "claude-4-sonnet", "claude-3-7-sonnet", + "claude-3-5-sonnet", + "openai-gpt-4.1", "openai-gpt-5", "openai-gpt-5.1", "openai-gpt-5.2", + "openai-gpt-5-mini", "openai-gpt-5-nano", "openai-gpt-5-chat", +]) // --------------------------------------------------------------------------- // parseSnowflakePAT @@ -78,7 +90,7 @@ describe("parseSnowflakePAT", () => { describe("transformSnowflakeBody", () => { test("rewrites max_tokens to max_completion_tokens", () => { const input = JSON.stringify({ model: "claude-sonnet-4-6", messages: [], max_tokens: 1000 }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.max_completion_tokens).toBe(1000) expect(parsed.max_tokens).toBeUndefined() @@ -86,7 +98,7 @@ describe("transformSnowflakeBody", () => { test("leaves requests without max_tokens unchanged", () => { const input = JSON.stringify({ model: "claude-sonnet-4-6", messages: [], max_completion_tokens: 1000 }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.max_completion_tokens).toBe(1000) expect(parsed.max_tokens).toBeUndefined() @@ -99,7 +111,7 @@ describe("transformSnowflakeBody", () => { tools: [{ type: "function", function: { name: "read_file" } }], tool_choice: "auto", }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.tools).toBeUndefined() expect(parsed.tool_choice).toBeUndefined() @@ -111,7 +123,7 @@ describe("transformSnowflakeBody", () => { messages: [{ role: "user", content: "hello" }], tools: [{ type: "function", function: { name: "read_file" } }], }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.tools).toBeUndefined() }) @@ -122,7 +134,7 @@ describe("transformSnowflakeBody", () => { messages: [{ role: "user", content: "hello" }], tools: [{ type: "function", function: { name: "read_file" } }], }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.tools).toBeUndefined() }) @@ -133,7 +145,7 @@ describe("transformSnowflakeBody", () => { messages: [{ role: "user", content: "hello" }], tools: [{ type: "function", function: { name: "read_file" } }], }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.tools).toBeDefined() expect(parsed.tools).toHaveLength(1) @@ -145,7 +157,7 @@ describe("transformSnowflakeBody", () => { messages: [{ role: "user", content: "hello" }], tools: [{ type: "function", function: { name: "read_file" } }], }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.tools).toBeDefined() expect(parsed.tools).toHaveLength(1) @@ -160,7 +172,7 @@ describe("transformSnowflakeBody", () => { ], tools: [{ type: "function", function: { name: "read_file" } }], }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) expect(syntheticStop).toBeDefined() expect(syntheticStop!.status).toBe(200) expect(syntheticStop!.headers.get("content-type")).toBe("text/event-stream") @@ -175,7 +187,7 @@ describe("transformSnowflakeBody", () => { ], tools: [{ type: "function", function: { name: "read_file" } }], }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) expect(syntheticStop).toBeUndefined() }) @@ -185,7 +197,7 @@ describe("transformSnowflakeBody", () => { messages: [{ role: "user", content: "test" }], tools: [{ type: "function", function: { name: "read_file" } }], }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) expect(syntheticStop).toBeUndefined() }) @@ -198,7 +210,7 @@ describe("transformSnowflakeBody", () => { { role: "assistant", content: "I'm here!" }, ], }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) expect(syntheticStop).toBeUndefined() }) @@ -211,7 +223,7 @@ describe("transformSnowflakeBody", () => { { role: "assistant", content: "I'm here!" }, ], }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) expect(syntheticStop).toBeDefined() }) @@ -223,7 +235,7 @@ describe("transformSnowflakeBody", () => { { role: "assistant", content: "I'm here!" }, ], }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) expect(syntheticStop).toBeDefined() }) @@ -235,7 +247,7 @@ describe("transformSnowflakeBody", () => { { role: "assistant", content: "done", tool_calls: [] }, ], }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) expect(syntheticStop).toBeDefined() }) @@ -250,7 +262,7 @@ describe("transformSnowflakeBody", () => { ], tools: [{ type: "function", function: { name: "read_file" } }], }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.tools).toBeUndefined() // tool_calls should be removed from assistant messages @@ -262,7 +274,7 @@ describe("transformSnowflakeBody", () => { }) test("throws on invalid JSON input", () => { - expect(() => transformSnowflakeBody("not-json")).toThrow() + expect(() => transformSnowflakeBody("not-json", TOOLCAPABLE_FIXTURE)).toThrow() }) test("synthetic stop SSE stream has correct format", async () => { @@ -273,7 +285,7 @@ describe("transformSnowflakeBody", () => { { role: "assistant", content: "done" }, ], }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) expect(syntheticStop).toBeDefined() const text = await syntheticStop!.text() // Should contain SSE data lines and [DONE] @@ -286,13 +298,13 @@ describe("transformSnowflakeBody", () => { test("handles empty messages array without crashing", () => { const input = JSON.stringify({ model: "claude-sonnet-4-6", messages: [] }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) expect(syntheticStop).toBeUndefined() }) test("handles missing messages field", () => { const input = JSON.stringify({ model: "claude-sonnet-4-6" }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) expect(JSON.parse(body).model).toBe("claude-sonnet-4-6") }) @@ -302,7 +314,7 @@ describe("transformSnowflakeBody", () => { messages: [{ role: "user", content: "test" }], max_completion_tokens: 500, }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.max_completion_tokens).toBe(500) expect(parsed.max_tokens).toBeUndefined() @@ -315,7 +327,7 @@ describe("transformSnowflakeBody", () => { max_tokens: 100, max_completion_tokens: 500, }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.max_completion_tokens).toBe(100) expect(parsed.max_tokens).toBeUndefined() @@ -327,7 +339,7 @@ describe("transformSnowflakeBody", () => { messages: [{ role: "user", content: "hello" }], tools: [{ type: "function", function: { name: "read_file" } }], }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.tools).toBeUndefined() }) @@ -338,7 +350,7 @@ describe("transformSnowflakeBody", () => { messages: [{ role: "user", content: "hello" }], tool_choice: "auto", }) - const { body } = transformSnowflakeBody(input) + const { body } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) const parsed = JSON.parse(body) expect(parsed.tool_choice).toBeUndefined() }) @@ -362,7 +374,7 @@ describe("SnowflakeCortexAuthPlugin fetch interceptor", () => { }) // Transform body (same logic as the fetch wrapper) - const result = transformSnowflakeBody(originalBody) + const result = transformSnowflakeBody(originalBody, TOOLCAPABLE_FIXTURE) const newBody = result.body // Body changed (max_tokens → max_completion_tokens), so lengths differ @@ -382,7 +394,7 @@ describe("SnowflakeCortexAuthPlugin fetch interceptor", () => { { role: "assistant", content: "response" }, ], }) - const { syntheticStop } = transformSnowflakeBody(input) + const { syntheticStop } = transformSnowflakeBody(input, TOOLCAPABLE_FIXTURE) expect(syntheticStop).toBeInstanceOf(Response) expect(syntheticStop!.status).toBe(200) expect(syntheticStop!.headers.get("content-type")).toBe("text/event-stream") @@ -588,8 +600,9 @@ describe("snowflake-cortex provider", () => { test("models added per Snowflake regional availability docs (issue #851)", async () => { // Regression: PR for issue #851 added 8 models that Snowflake Cortex - // supports but were missing from the hardcoded list. Lock them in so a - // future refactor doesn't silently drop them. + // supports but were missing from the hardcoded list. Lock in identity, + // toolcall capability, AND limits (the limits were corrected in the + // consensus-review follow-up after an initial drift was caught). await setupOAuth() try { await using tmp = await tmpdir({ @@ -602,27 +615,161 @@ describe("snowflake-cortex provider", () => { fn: async () => { const providers = await Provider.list() const models = providers["snowflake-cortex"].models - // Claude (toolcall: true) - expect(models["claude-opus-4-7"]).toBeDefined() - expect(models["claude-opus-4-7"].capabilities.toolcall).toBe(true) - // OpenAI (toolcall: true) - expect(models["openai-gpt-5.1"]).toBeDefined() - expect(models["openai-gpt-5.1"].capabilities.toolcall).toBe(true) - expect(models["openai-gpt-5.2"]).toBeDefined() - expect(models["openai-gpt-5.2"].capabilities.toolcall).toBe(true) - // Llama (toolcall: false) - expect(models["llama4-scout"]).toBeDefined() - expect(models["llama4-scout"].capabilities.toolcall).toBe(false) - expect(models["llama3.3-70b"]).toBeDefined() - expect(models["llama3.3-70b"].capabilities.toolcall).toBe(false) - expect(models["snowflake-llama-3.1-405b"]).toBeDefined() - expect(models["snowflake-llama-3.1-405b"].capabilities.toolcall).toBe(false) - // Mixtral (toolcall: false) - expect(models["mixtral-8x7b"]).toBeDefined() - expect(models["mixtral-8x7b"].capabilities.toolcall).toBe(false) - // Gemini (toolcall: false — unverified on Cortex) - expect(models["gemini-3.1-pro"]).toBeDefined() - expect(models["gemini-3.1-pro"].capabilities.toolcall).toBe(false) + + // Each entry: [id, expected toolcall, expected context, expected output] + // Values sourced from + // https://docs.snowflake.com/en/user-guide/snowflake-cortex/aisql-regional-availability + // (openai-gpt-5.2 is not in the restrictions table; using gpt-5 family defaults.) + const expectations: Array<[string, boolean, number, number]> = [ + ["claude-opus-4-7", true, 1000000, 128000], + ["openai-gpt-5.1", true, 272000, 8192], + ["openai-gpt-5.2", true, 272000, 8192], + ["llama4-scout", false, 128000, 8192], + ["llama3.3-70b", false, 128000, 8192], + ["snowflake-llama-3.1-405b", false, 8000, 8192], + ["mixtral-8x7b", false, 32000, 8192], + ["gemini-3.1-pro", false, 1000000, 64000], + ] + + for (const [id, toolcall, context, output] of expectations) { + expect(models[id], `model ${id} should be defined`).toBeDefined() + expect(models[id].capabilities.toolcall, `${id} toolcall`).toBe(toolcall) + expect(models[id].limit.context, `${id} context`).toBe(context) + expect(models[id].limit.output, `${id} output`).toBe(output) + } + }, + }) + } finally { + await restoreAuth() + } + }) + + test("buildToolCapableSet derives the allowlist from provider model capabilities", async () => { + // Source-of-truth test for the escape-hatch fix: the request transform + // gets its allowlist from `provider.models.capabilities.toolcall` rather + // than a separate hardcoded set in snowflake.ts. Models added via + // opencode.json with `tool_call: true` therefore retain tools at request + // time, and the picker capability cannot drift from the transform behavior. + await setupOAuth() + try { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://altimate.ai/config.json" })) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const set = buildToolCapableSet(providers["snowflake-cortex"].models) + // Every model with capabilities.toolcall === true is in the set; the rest are not. + for (const [id, m] of Object.entries(providers["snowflake-cortex"].models)) { + expect(set.has(id), `${id} parity`).toBe(m.capabilities.toolcall) + } + }, + }) + } finally { + await restoreAuth() + } + }) + + test("escape-hatch model with tool_call: true retains tools through transformSnowflakeBody", async () => { + // The documented opencode.json escape hatch must work end-to-end: picker + // shows the model as tool-capable AND the request transform passes tools + // through. Without the loader-derived allowlist this test would fail + // because the static set never sees user-added entries. + await setupOAuth() + try { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://altimate.ai/config.json", + provider: { + "snowflake-cortex": { + models: { + "user-tool-model": { + name: "User Tool Model", + limit: { context: 100000, output: 8192 }, + tool_call: true, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const toolCapable = buildToolCapableSet(providers["snowflake-cortex"].models) + const input = JSON.stringify({ + model: "user-tool-model", + messages: [{ role: "user", content: "hi" }], + tools: [{ type: "function", function: { name: "read_file" } }], + tool_choice: "auto", + }) + const { body } = transformSnowflakeBody(input, toolCapable) + const parsed = JSON.parse(body) + expect(parsed.tools).toBeDefined() + expect(parsed.tools).toHaveLength(1) + expect(parsed.tool_choice).toBe("auto") + }, + }) + } finally { + await restoreAuth() + } + }) + + test("escape-hatch model with tool_call: false has tools stripped through transformSnowflakeBody", async () => { + // Counterpart to the above: a user-registered non-tool model gets the + // tools stripped just like the built-in Llama/Mistral entries do. + await setupOAuth() + try { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://altimate.ai/config.json", + provider: { + "snowflake-cortex": { + models: { + "user-notool-model": { + name: "User No-Tool Model", + limit: { context: 32000, output: 4096 }, + tool_call: false, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const toolCapable = buildToolCapableSet(providers["snowflake-cortex"].models) + const input = JSON.stringify({ + model: "user-notool-model", + messages: [ + { role: "user", content: "hi" }, + { role: "tool", content: "x", tool_call_id: "t1" }, + ], + tools: [{ type: "function", function: { name: "read_file" } }], + tool_choice: "auto", + }) + const { body } = transformSnowflakeBody(input, toolCapable) + const parsed = JSON.parse(body) + expect(parsed.tools).toBeUndefined() + expect(parsed.tool_choice).toBeUndefined() + // Orphaned tool messages dropped too. + expect(parsed.messages.find((m: { role: string }) => m.role === "tool")).toBeUndefined() }, }) } finally { From 950bda5f0acc4d3d75a27eb2a6ec0570475ab373 Mon Sep 17 00:00:00 2001 From: Haider Date: Mon, 1 Jun 2026 17:27:29 +0530 Subject: [PATCH 3/5] docs: use canonical `altimate-code.json` config filename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer caught my providers.md addition referring to `opencode.json` (and `.opencode/opencode.json`). Both names are accepted by the config loader (config.ts:1541 candidates: `altimate-code.json`, `opencode.jsonc`, `opencode.json`, `config.json`) but the rest of the docs standardize on `altimate-code.json` — `docs/docs/configure/config.md` uses the canonical name 9× and `opencode.json` 0×. My section was the inconsistent one. Root cause: I used `opencode.json` because that's the upstream project's name and what the snowflake tests happen to use. But this is the altimate-code fork; user-facing docs use the fork's canonical name. Updates `providers.md` "Adding a model not in the list" section and the three `altimate_change`-introduced comments in `snowflake.ts` to use `altimate-code.json` consistently. Tests left using `opencode.json` literals — they're exercising the loader (which accepts both) and swapping them would be churn without behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/docs/configure/providers.md | 4 ++-- packages/opencode/src/altimate/plugin/snowflake.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/docs/configure/providers.md b/docs/docs/configure/providers.md index d987ec659b..be552e6de1 100644 --- a/docs/docs/configure/providers.md +++ b/docs/docs/configure/providers.md @@ -291,7 +291,7 @@ Billing flows through your Snowflake credits — no per-token costs. ### Adding a model not in the list -Snowflake Cortex adds models faster than this list can be updated. If a model is available on your Cortex account but not yet listed above, you can register it locally without forking the CLI — add it under `provider["snowflake-cortex"].models` in your `opencode.json` (or `.opencode/opencode.json`): +Snowflake Cortex adds models faster than this list can be updated. If a model is available on your Cortex account but not yet listed above, you can register it locally without forking the CLI — add it under `provider["snowflake-cortex"].models` in your `altimate-code.json` (or `.altimate-code/altimate-code.json`): ```json { @@ -311,7 +311,7 @@ Snowflake Cortex adds models faster than this list can be updated. If a model is The entry merges with the built-in list, so the model appears in the picker and can be selected as `snowflake-cortex/your-new-model-id`. Set `"tool_call": false` for models that don't support tools on Cortex (Llama, Mistral, DeepSeek, Gemini today) — otherwise requests with tools will fail. -The `tool_call` field uses snake_case (matching the rest of the `opencode.json` schema) and maps to the picker's `capabilities.toolcall`. The request transform reads the same value, so a user-added model marked `tool_call: true` keeps `tools` and `tool_choice` in outgoing requests — and one marked `tool_call: false` has them stripped, the same as the built-in non-tool entries. +The `tool_call` field uses snake_case (matching the rest of the `altimate-code.json` schema) and maps to the picker's `capabilities.toolcall`. The request transform reads the same value, so a user-added model marked `tool_call: true` keeps `tools` and `tool_choice` in outgoing requests — and one marked `tool_call: false` has them stripped, the same as the built-in non-tool entries. ## Databricks AI Gateway diff --git a/packages/opencode/src/altimate/plugin/snowflake.ts b/packages/opencode/src/altimate/plugin/snowflake.ts index d2582c288c..d2dbe7b41d 100644 --- a/packages/opencode/src/altimate/plugin/snowflake.ts +++ b/packages/opencode/src/altimate/plugin/snowflake.ts @@ -6,7 +6,7 @@ import { Auth, OAUTH_DUMMY_KEY } from "@/auth" * 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 `opencode.json`) is the single source of truth. + * via `altimate-code.json`) is the single source of truth. * * Snowflake's documented constraint: only Claude and OpenAI models accept * tools on Cortex; everything else rejects with "tool calling is not supported." @@ -41,7 +41,7 @@ export function parseSnowflakePAT(code: string): { account: string; token: strin * * @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 `opencode.json` are honored. + * user-added models with `tool_call: true` in `altimate-code.json` are honored. */ export function transformSnowflakeBody( bodyText: string, @@ -115,10 +115,10 @@ export async function SnowflakeCortexAuthPlugin(_input: PluginInput): Promise Date: Mon, 1 Jun 2026 17:33:04 +0530 Subject: [PATCH 4/5] test: use tmpdir({ config }) helper instead of manual Bun.write in init Bot review nit from CodeRabbit on the second review pass: tests added in this PR were calling `tmpdir({ init: async (dir) => Bun.write(...) })` to seed a project config, but the fixture already exposes a dedicated `config` option that does exactly that. Switching to the helper is shorter, matches the codebase guideline, and centralizes the `$schema` boilerplate. Affects the 5 tests added by this PR: - models added per Snowflake regional availability docs (issue #851) - buildToolCapableSet derives the allowlist from provider model capabilities - user can register a model not in the hardcoded list via altimate-code.json (also renamed from `via opencode.json` for canonical consistency with the docs) - escape-hatch model with tool_call: true retains tools through transformSnowflakeBody - escape-hatch model with tool_call: false has tools stripped through transformSnowflakeBody Pre-existing tests in the same file follow the old pattern; left out of scope to avoid churn in unrelated code. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../opencode/test/provider/snowflake.test.ts | 108 +++++++----------- 1 file changed, 41 insertions(+), 67 deletions(-) diff --git a/packages/opencode/test/provider/snowflake.test.ts b/packages/opencode/test/provider/snowflake.test.ts index 0c23593041..9a5870ed16 100644 --- a/packages/opencode/test/provider/snowflake.test.ts +++ b/packages/opencode/test/provider/snowflake.test.ts @@ -605,11 +605,7 @@ describe("snowflake-cortex provider", () => { // consensus-review follow-up after an initial drift was caught). await setupOAuth() try { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://altimate.ai/config.json" })) - }, - }) + await using tmp = await tmpdir({ config: {} }) await Instance.provide({ directory: tmp.path, fn: async () => { @@ -648,15 +644,11 @@ describe("snowflake-cortex provider", () => { // Source-of-truth test for the escape-hatch fix: the request transform // gets its allowlist from `provider.models.capabilities.toolcall` rather // than a separate hardcoded set in snowflake.ts. Models added via - // opencode.json with `tool_call: true` therefore retain tools at request - // time, and the picker capability cannot drift from the transform behavior. + // altimate-code.json with `tool_call: true` therefore retain tools at + // request time, and the picker capability cannot drift from the transform. await setupOAuth() try { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://altimate.ai/config.json" })) - }, - }) + await using tmp = await tmpdir({ config: {} }) await Instance.provide({ directory: tmp.path, fn: async () => { @@ -674,32 +666,26 @@ describe("snowflake-cortex provider", () => { }) test("escape-hatch model with tool_call: true retains tools through transformSnowflakeBody", async () => { - // The documented opencode.json escape hatch must work end-to-end: picker - // shows the model as tool-capable AND the request transform passes tools - // through. Without the loader-derived allowlist this test would fail + // The documented altimate-code.json escape hatch must work end-to-end: + // picker shows the model as tool-capable AND the request transform passes + // tools through. Without the loader-derived allowlist this test would fail // because the static set never sees user-added entries. await setupOAuth() try { await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://altimate.ai/config.json", - provider: { - "snowflake-cortex": { - models: { - "user-tool-model": { - name: "User Tool Model", - limit: { context: 100000, output: 8192 }, - tool_call: true, - }, - }, + config: { + provider: { + "snowflake-cortex": { + models: { + "user-tool-model": { + name: "User Tool Model", + limit: { context: 100000, output: 8192 }, + tool_call: true, }, }, - }), - ) - }, + }, + }, + } as any, }) await Instance.provide({ directory: tmp.path, @@ -730,25 +716,19 @@ describe("snowflake-cortex provider", () => { await setupOAuth() try { await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://altimate.ai/config.json", - provider: { - "snowflake-cortex": { - models: { - "user-notool-model": { - name: "User No-Tool Model", - limit: { context: 32000, output: 4096 }, - tool_call: false, - }, - }, + config: { + provider: { + "snowflake-cortex": { + models: { + "user-notool-model": { + name: "User No-Tool Model", + limit: { context: 32000, output: 4096 }, + tool_call: false, }, }, - }), - ) - }, + }, + }, + } as any, }) await Instance.provide({ directory: tmp.path, @@ -777,32 +757,26 @@ describe("snowflake-cortex provider", () => { } }) - test("user can register a model not in the hardcoded list via opencode.json", async () => { + test("user can register a model not in the hardcoded list via altimate-code.json", async () => { // Documents the option (2) escape hatch: when Snowflake adds a model // before the CLI's hardcoded list catches up, users add it under // provider['snowflake-cortex'].models and it merges into the picker. await setupOAuth() try { await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://altimate.ai/config.json", - provider: { - "snowflake-cortex": { - models: { - "future-model-x": { - name: "Future Model X", - limit: { context: 200000, output: 32000 }, - tool_call: true, - }, - }, + config: { + provider: { + "snowflake-cortex": { + models: { + "future-model-x": { + name: "Future Model X", + limit: { context: 200000, output: 32000 }, + tool_call: true, }, }, - }), - ) - }, + }, + }, + } as any, }) await Instance.provide({ directory: tmp.path, From c0999aeb5641651fc8f569edd80d52673c76a54d Mon Sep 17 00:00:00 2001 From: Haider Date: Mon, 1 Jun 2026 23:58:13 +0530 Subject: [PATCH 5/5] fix: bot-review findings on snowflake-cortex PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three valid findings from cubic + a human reviewer's auto-discovery documentation request, addressed in one commit: 1. **`snowflake-llama-3.1-405b` output > context (cubic P2 on provider.ts:1110).** Snowflake's docs list output=8192 for this model, but its context is 8000. Output can never physically exceed the total token budget, so a max_tokens=8192 request would either be rejected or implicitly capped by the gateway. Capped at 4096 (the original sibling default), with a comment explaining the apparent doc inconsistency. 2. **Aliased models bypass the tool-call allowlist (cubic P2 on snowflake.ts:19).** `buildToolCapableSet` indexed by the picker map key, but the request body sends `model: `. A user registering an alias like: "my-claude-alias": { "id": "claude-opus-4-7", "tool_call": true } would have tools silently stripped because "my-claude-alias" != "claude-opus-4-7" in the set. Updated the helper to also include `m.id` and `m.api?.id` so the lookup matches whichever form the transform sees. Added a regression test that exercises the alias path end-to-end (config registration → loader-built set → request transform with the api.id form). 3. **Document why auto-discovery isn't used (web-researcher suggestion on provider.ts:1050).** Added a comment near the snowflake-cortex registration explaining that Cortex's models endpoint (`GET /api/v2/cortex/v1/models`) has non-standard semantics (requires JSON body on GET) that break standard OpenAI-compatible clients — so the hardcoded list is the source of truth until a probe-able shim exists. Skipped: - "CVE-2026-12345" comment from the web-researcher reviewer — the CVE id is a placeholder/fake (no such advisory exists), suggestion body is vague. Not actioned. - Schema-validation hardening for user-provided model configs — reasonable but broader than this PR's scope. CI status: the one TypeScript failure on this branch (`S27: sql_analyze with parse error`) is pre-existing on `main` HEAD `6ad8b47` — unrelated to snowflake-cortex. Tests: 55 → 56 in `test/provider/snowflake.test.ts`; typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../opencode/src/altimate/plugin/snowflake.ts | 22 +++++++- packages/opencode/src/provider/provider.ts | 14 ++++- .../opencode/test/provider/snowflake.test.ts | 56 ++++++++++++++++++- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/altimate/plugin/snowflake.ts b/packages/opencode/src/altimate/plugin/snowflake.ts index d2dbe7b41d..8528a0c328 100644 --- a/packages/opencode/src/altimate/plugin/snowflake.ts +++ b/packages/opencode/src/altimate/plugin/snowflake.ts @@ -8,15 +8,31 @@ import { Auth, OAUTH_DUMMY_KEY } from "@/auth" * 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, + models: Record< + string, + { + id?: string + api?: { id?: string } + capabilities: { toolcall: boolean } + } + >, ): ReadonlySet { const set = new Set() - for (const [id, m] of Object.entries(models)) { - if (m.capabilities.toolcall) set.add(id) + 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 } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index eef199d3b9..d151499aa7 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -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", @@ -1103,11 +1111,13 @@ export namespace Provider { { toolcall: false }, ), // snowflake-llama-3.1-405b: 8k context per Snowflake docs (much smaller - // than the upstream Meta model's window). + // 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: 8192 }, + { context: 8000, output: 4096 }, { toolcall: false }, ), "llama3.1-70b": makeSnowflakeModel( diff --git a/packages/opencode/test/provider/snowflake.test.ts b/packages/opencode/test/provider/snowflake.test.ts index 9a5870ed16..2b09ff9e2c 100644 --- a/packages/opencode/test/provider/snowflake.test.ts +++ b/packages/opencode/test/provider/snowflake.test.ts @@ -622,7 +622,10 @@ describe("snowflake-cortex provider", () => { ["openai-gpt-5.2", true, 272000, 8192], ["llama4-scout", false, 128000, 8192], ["llama3.3-70b", false, 128000, 8192], - ["snowflake-llama-3.1-405b", false, 8000, 8192], + // Snowflake docs list output=8192 for this model, but its context + // is only 8000 — capped at 4096 (sibling default) so prompt+output + // always fit. See provider.ts comment for the rationale. + ["snowflake-llama-3.1-405b", false, 8000, 4096], ["mixtral-8x7b", false, 32000, 8192], ["gemini-3.1-pro", false, 1000000, 64000], ] @@ -757,6 +760,57 @@ describe("snowflake-cortex provider", () => { } }) + test("aliased model (picker key != api.id) is matched by buildToolCapableSet on both ids", async () => { + // Regression: when a user registers an alias like + // `"my-claude-alias": { "id": "claude-opus-4-7", "tool_call": true }`, + // the picker map is keyed by "my-claude-alias" but the request body sends + // `model: "claude-opus-4-7"`. The allowlist must include BOTH so tools + // aren't silently stripped on the way out. + await setupOAuth() + try { + await using tmp = await tmpdir({ + config: { + provider: { + "snowflake-cortex": { + models: { + "my-claude-alias": { + id: "claude-opus-4-7", + name: "My Claude Alias", + limit: { context: 1000000, output: 128000 }, + tool_call: true, + }, + }, + }, + }, + } as any, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const toolCapable = buildToolCapableSet(providers["snowflake-cortex"].models) + // Both forms must be in the set. + expect(toolCapable.has("my-claude-alias")).toBe(true) + expect(toolCapable.has("claude-opus-4-7")).toBe(true) + + // And the transform must keep tools when the request uses the api.id form. + const input = JSON.stringify({ + model: "claude-opus-4-7", + messages: [{ role: "user", content: "hi" }], + tools: [{ type: "function", function: { name: "read_file" } }], + tool_choice: "auto", + }) + const { body } = transformSnowflakeBody(input, toolCapable) + const parsed = JSON.parse(body) + expect(parsed.tools).toBeDefined() + expect(parsed.tool_choice).toBe("auto") + }, + }) + } finally { + await restoreAuth() + } + }) + test("user can register a model not in the hardcoded list via altimate-code.json", async () => { // Documents the option (2) escape hatch: when Snowflake adds a model // before the CLI's hardcoded list catches up, users add it under