Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions docs/docs/configure/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,15 +280,39 @@ Billing flows through your Snowflake credits — no per-token costs.

Copy link
Copy Markdown

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

| 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 `altimate-code.json` (or `.altimate-code/altimate-code.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.

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

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.
Expand Down
66 changes: 53 additions & 13 deletions packages/opencode/src/altimate/plugin/snowflake.ts
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._-]+$/
Expand All @@ -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
Expand All @@ -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)) {
Expand Down Expand Up @@ -97,6 +129,14 @@ export async function SnowflakeCortexAuthPlugin(_input: PluginInput): Promise<Ho
model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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) {
Expand Down Expand Up @@ -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")
Expand Down
59 changes: 58 additions & 1 deletion packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
Expand All @@ -1049,6 +1058,16 @@ export namespace Provider {
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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", {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
context: 272000,
output: 8192,
}),
Comment thread
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,
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions packages/opencode/test/altimate/cortex-snowflake-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ import {
VALID_ACCOUNT_RE,
} from "../../src/altimate/plugin/snowflake"

// Production builds this set from `provider.models` at loader time; mirror
// the same shape here for the transform-only e2e checks.
const TOOLCAPABLE_E2E_FIXTURE: ReadonlySet<string> = new Set([
"claude-3-5-sonnet", "claude-sonnet-4-6", "claude-opus-4-7",
"openai-gpt-4.1", "openai-gpt-5",
])

// ---------------------------------------------------------------------------
// Auth configuration
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
})

Expand All @@ -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)
})
Expand Down
Loading
Loading