Skip to content

Commit c4f5c16

Browse files
mdesmetclaudeanandgupta42
authored
feat: add Snowflake Cortex as an AI provider (#349)
* feat: add Snowflake Cortex as an AI provider Adds `snowflake-cortex` as a built-in provider using Programmatic Access Token (PAT) auth. Users authenticate by entering `<account>::<token>` once; billing flows through Snowflake credits. Includes Claude, Llama, Mistral, and DeepSeek models with Cortex-specific request transforms (max_completion_tokens, tool stripping for unsupported models, synthetic SSE stop to break the AI SDK's continuation-check loop when Snowflake rejects trailing assistant messages). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: harden Snowflake Cortex provider with `altimate_change` markers and edge case fixes - Add `altimate_change` markers to all upstream-shared files (`provider.ts`, `schema.ts`, `plugin/index.ts`) to prevent overwrites on upstream merges - Validate account ID against `^[a-zA-Z0-9._-]+$` to prevent URL injection - Remove `(auth as any).accountId` casts — use proper type narrowing - Fix `env` array: `SNOWFLAKE_PAT` → `SNOWFLAKE_ACCOUNT` (matches actual usage) - Fix `claude-3-5-sonnet` output limit: `8096` → `8192` - Strip orphaned `tool_calls` and `tool` role messages for no-toolcall models - Use explicit `Array.isArray(tool_calls)` check for synthetic stop condition - Remove zero-usage block from synthetic SSE to avoid broken token accounting - Handle `ArrayBuffer` body type in fetch wrapper - Reduce PAT expiry from 365 → 90 days (matches Snowflake default TTL) - Add 14 new test cases covering URL injection, orphaned tool_calls, empty tool_calls array, SSE format validation, missing messages, and env/output limits Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: require oauth auth for snowflake-cortex, don't expose account as credential Addresses CodeRabbit review comments: - Require `auth.type === "oauth"` before autoloading — env-only `SNOWFLAKE_ACCOUNT` no longer makes the provider look configured without a PAT - Set `env: []` so `state()` env-key scan doesn't treat account name as an API key - Validate account from env fallback against `^[a-zA-Z0-9._-]+$` - Add test: env-only without oauth does NOT load the provider - All provider tests now set up/teardown oauth auth properly via save/restore - Update env array assertion: `toContain("SNOWFLAKE_ACCOUNT")` → `toEqual([])` Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address consensus code review findings Fixes from 6-model consensus review (Claude, GPT 5.2, Gemini 3.1, Kimi K2.5, MiniMax M2.5, GLM-5): - Gate synthetic SSE stop on `stream !== false` to avoid returning SSE format for non-streaming requests (Major, flagged by GPT 5.2 Codex) - Delete `content-length` header after body mutation to prevent length mismatch (Minor, flagged by GPT 5.2/Kimi/Gemini consensus) - Export `VALID_ACCOUNT_RE` from `snowflake.ts` and import in `provider.ts` to eliminate duplicated regex (Minor, flagged by GLM-5) - Add `claude-3-5-sonnet` to toolcall capability test (Kimi K2.5) - Add 3 new tests: `stream: false` skips synthetic stop, `stream: true` triggers it, absent `stream` field defaults to streaming behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add Cortex E2E tests and sanitize hardcoded credentials - Add `cortex-snowflake-e2e.test.ts` with 16 E2E tests for the Snowflake Cortex AI provider: PAT auth, streaming/non-streaming completions, model availability, request transforms, assistant-last rejection, PAT parsing - Tests skip via `describe.skipIf` when `SNOWFLAKE_CORTEX_PAT` is not set - Remove hardcoded credentials from `drivers-snowflake-e2e.test.ts` docstring — replaced with placeholder values Run with: export SNOWFLAKE_CORTEX_ACCOUNT="<account>" export SNOWFLAKE_CORTEX_PAT="<pat>" bun test test/altimate/cortex-snowflake-e2e.test.ts --timeout 120000 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update Cortex models from E2E testing against real Snowflake API Verified against Snowflake account ejjkbko-fub20041 with cross-region inference enabled. Key findings and fixes: Model list changes (from real API probing): - Replace `llama3.3-70b` (unavailable) with `snowflake-llama-3.3-70b` - Add `llama3.1-70b`, `llama3.1-405b`, `llama3.1-8b`, `mistral-7b` - All 10 models verified against live Cortex endpoint Tool calling fix: - Switch from blocklist (`NO_TOOLCALL_MODELS`) to allowlist (`TOOLCALL_MODELS`) — only Claude models support tool calls on Cortex, all others reject with "tool calling is not supported" E2E test improvements (24 tests, all pass against live API): - Test all 10 registered models for availability and response shape - Tool call support test: Claude accepts, non-Claude rejects - DeepSeek R1 reasoning format test (`<think>` tags in content) - Support key-pair JWT auth (no PAT required) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add OpenAI and additional Claude models from Snowflake Cortex docs From official Snowflake documentation (docs.snowflake.com): New models added (19 → 28 total): - OpenAI: `openai-gpt-4.1`, `openai-gpt-5`, `openai-gpt-5-mini`, `openai-gpt-5-nano`, `openai-gpt-5-chat`, `openai-gpt-oss-120b` - Claude: `claude-opus-4-6`, `claude-sonnet-4-5`, `claude-opus-4-5`, `claude-4-sonnet`, `claude-4-opus`, `claude-3-7-sonnet` - Meta: `llama4-maverick`, `mistral-large` Tool calling update: - Per docs: "Tool calling is supported for OpenAI and Claude models only" - Updated `TOOLCALL_MODELS` allowlist to include all OpenAI + Claude IDs Note: OpenAI models were not available on this test account (returned "unknown model") but are documented in the Cortex REST API reference. Availability depends on region and account configuration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: verify model availability via live API, remove broken models Probed all 28 documented models against ejjkbko-fub20041 with cross-region enabled: Verified working (13): - 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-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 tool calling confirmed working (get_weather test) Removed from registration (kept as comments): - claude-4-opus: 403 "account not allowed" (gated) - openai-gpt-oss-120b: 500 internal error (not stable) Also verified: - llama4-maverick, mistral-large: working - GPT-5 preview variants return 200 but empty content (preview) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs+test: pre-release — docs, test gaps, and full model validation Documentation: - Add Snowflake Cortex section to docs/configure/providers.md with auth instructions, model table, and cross-region note - Add Snowflake Cortex to model format reference in models.md - Add v0.5.6 changelog entry Test gap fixes (46 → 52 unit tests): - Content-length deletion after body transform - Synthetic stop returns valid SSE Response object - Both max_tokens + max_completion_tokens present (max_tokens wins) - Unknown model tools stripped (allowlist default) - tool_choice without tools stripped for non-toolcall models - max_completion_tokens preserved when max_tokens absent E2E model validation (37 pass against live API): - All 26 registered models probed: 21 available, 4 gated/broken, 1 preview - Accept 200/400/403/500 for model availability (accounts vary) - Handle preview models returning empty content (openai-gpt-5-*) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: anandgupta42 <anand@altimate.ai>
1 parent a29f212 commit c4f5c16

File tree

10 files changed

+1372
-2
lines changed

10 files changed

+1372
-2
lines changed

docs/docs/configure/models.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Models are referenced as `provider/model-name`:
7979
| Ollama | `ollama/llama3.1` |
8080
| OpenRouter | `openrouter/anthropic/claude-sonnet-4-6` |
8181
| Copilot | `copilot/gpt-4o` |
82+
| Snowflake Cortex | `snowflake-cortex/claude-sonnet-4-6` |
8283
| Custom | `my-provider/my-model` |
8384

8485
See [Providers](providers.md) for full provider configuration details.

docs/docs/configure/providers.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,36 @@ Access 150+ models through a single API key.
176176

177177
Uses your GitHub Copilot subscription. Authenticate with `altimate auth`.
178178

179+
## Snowflake Cortex
180+
181+
```json
182+
{
183+
"provider": {
184+
"snowflake-cortex": {}
185+
},
186+
"model": "snowflake-cortex/claude-sonnet-4-6"
187+
}
188+
```
189+
190+
Authenticate with `altimate auth snowflake-cortex` using a Programmatic Access Token (PAT). Enter credentials as `account-identifier::pat-token`.
191+
192+
Create a PAT in Snowsight: **Admin > Security > Programmatic Access Tokens**.
193+
194+
Billing flows through your Snowflake credits — no per-token costs.
195+
196+
**Available models:**
197+
198+
| Model | Tool Calling |
199+
|-------|-------------|
200+
| `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 |
201+
| `openai-gpt-4.1`, `openai-gpt-5`, `openai-gpt-5-mini`, `openai-gpt-5-nano`, `openai-gpt-5-chat` | Yes |
202+
| `llama4-maverick`, `snowflake-llama-3.3-70b`, `llama3.1-70b`, `llama3.1-405b`, `llama3.1-8b` | No |
203+
| `mistral-large`, `mistral-large2`, `mistral-7b` | No |
204+
| `deepseek-r1` | No |
205+
206+
!!! note
207+
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.
208+
179209
## Custom / OpenAI-Compatible
180210

181211
Any OpenAI-compatible endpoint can be used as a provider:

docs/docs/reference/changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ After upgrading, the TUI welcome banner shows what changed since your previous v
2121

2222
---
2323

24+
## [0.5.6] - 2026-03-21
25+
26+
### Added
27+
28+
- Snowflake Cortex as a built-in AI provider with PAT authentication (#349)
29+
- 26 models: Claude, OpenAI, Llama, Mistral, DeepSeek
30+
- Tool calling support for Claude and OpenAI models
31+
- Zero token cost — billing via Snowflake credits
32+
- Cortex-specific request transforms (`max_completion_tokens`, tool stripping, synthetic stop)
33+
2434
## [0.5.0] - 2026-03-18
2535

2636
### Added
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
2+
import { Auth, OAUTH_DUMMY_KEY } from "@/auth"
3+
4+
// Only OpenAI and Claude models support tool calling on Snowflake Cortex.
5+
// All other models reject tools with "tool calling is not supported".
6+
const TOOLCALL_MODELS = new Set([
7+
// Claude
8+
"claude-sonnet-4-6", "claude-opus-4-6", "claude-sonnet-4-5", "claude-opus-4-5",
9+
"claude-haiku-4-5", "claude-4-sonnet", "claude-4-opus", "claude-3-7-sonnet", "claude-3-5-sonnet",
10+
// OpenAI
11+
"openai-gpt-4.1", "openai-gpt-5", "openai-gpt-5-mini", "openai-gpt-5-nano",
12+
"openai-gpt-5-chat", "openai-gpt-oss-120b", "openai-o4-mini",
13+
])
14+
15+
/** Snowflake account identifiers contain only alphanumeric, hyphen, underscore, and dot characters. */
16+
export const VALID_ACCOUNT_RE = /^[a-zA-Z0-9._-]+$/
17+
18+
/** Parse a `account::token` PAT credential string. */
19+
export function parseSnowflakePAT(code: string): { account: string; token: string } | null {
20+
const sep = code.indexOf("::")
21+
if (sep === -1) return null
22+
const account = code.substring(0, sep).trim()
23+
const token = code.substring(sep + 2).trim()
24+
if (!account || !token) return null
25+
if (!VALID_ACCOUNT_RE.test(account)) return null
26+
return { account, token }
27+
}
28+
29+
/**
30+
* Transform a Snowflake Cortex request body string.
31+
* Returns a Response to short-circuit the fetch (synthetic stop), or undefined to continue normally.
32+
*/
33+
export function transformSnowflakeBody(bodyText: string): { body: string; syntheticStop?: Response } {
34+
const parsed = JSON.parse(bodyText)
35+
36+
// Snowflake uses max_completion_tokens instead of max_tokens
37+
if ("max_tokens" in parsed) {
38+
parsed.max_completion_tokens = parsed.max_tokens
39+
delete parsed.max_tokens
40+
}
41+
42+
// Strip tools for models that don't support tool calling on Snowflake Cortex.
43+
// Also remove orphaned tool_calls from messages to avoid Snowflake API errors.
44+
if (!TOOLCALL_MODELS.has(parsed.model)) {
45+
delete parsed.tools
46+
delete parsed.tool_choice
47+
if (Array.isArray(parsed.messages)) {
48+
for (const msg of parsed.messages) {
49+
if (msg.tool_calls) delete msg.tool_calls
50+
}
51+
parsed.messages = parsed.messages.filter((msg: { role: string }) => msg.role !== "tool")
52+
}
53+
}
54+
55+
// Snowflake rejects requests where the last message is an assistant role.
56+
// The AI SDK makes "continuation check" requests with the model's last response
57+
// at the end. Stripping causes an infinite loop (same request → same response).
58+
// Instead, short-circuit by returning a synthetic "stop" streaming response.
59+
if (Array.isArray(parsed.messages)) {
60+
const last = parsed.messages.at(-1)
61+
if (parsed.stream !== false && last?.role === "assistant" && (!Array.isArray(last.tool_calls) || last.tool_calls.length === 0)) {
62+
const encoder = new TextEncoder()
63+
const chunks = [
64+
`data: {"id":"sf-done","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":null}]}\n\n`,
65+
`data: {"id":"sf-done","object":"chat.completion.chunk","choices":[{"delta":{},"index":0,"finish_reason":"stop"}]}\n\n`,
66+
`data: [DONE]\n\n`,
67+
]
68+
const stream = new ReadableStream({
69+
start(controller) {
70+
for (const chunk of chunks) controller.enqueue(encoder.encode(chunk))
71+
controller.close()
72+
},
73+
})
74+
return {
75+
body: JSON.stringify(parsed),
76+
syntheticStop: new Response(stream, {
77+
status: 200,
78+
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" },
79+
}),
80+
}
81+
}
82+
}
83+
84+
return { body: JSON.stringify(parsed) }
85+
}
86+
87+
export async function SnowflakeCortexAuthPlugin(_input: PluginInput): Promise<Hooks> {
88+
return {
89+
auth: {
90+
provider: "snowflake-cortex",
91+
async loader(getAuth, provider) {
92+
const auth = await getAuth()
93+
if (auth.type !== "oauth") return {}
94+
95+
// Zero costs (billed via Snowflake credits)
96+
for (const model of Object.values(provider.models)) {
97+
model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } }
98+
}
99+
100+
return {
101+
apiKey: OAUTH_DUMMY_KEY,
102+
async fetch(requestInput: RequestInfo | URL, init?: RequestInit) {
103+
const currentAuth = await getAuth()
104+
if (currentAuth.type !== "oauth") return fetch(requestInput, init)
105+
106+
const headers = new Headers()
107+
if (init?.headers) {
108+
if (init.headers instanceof Headers) {
109+
init.headers.forEach((value, key) => headers.set(key, value))
110+
} else if (Array.isArray(init.headers)) {
111+
for (const [key, value] of init.headers) {
112+
if (value !== undefined) headers.set(key, String(value))
113+
}
114+
} else {
115+
for (const [key, value] of Object.entries(init.headers)) {
116+
if (value !== undefined) headers.set(key, String(value))
117+
}
118+
}
119+
}
120+
121+
headers.set("authorization", `Bearer ${currentAuth.access}`)
122+
headers.set("X-Snowflake-Authorization-Token-Type", "PROGRAMMATIC_ACCESS_TOKEN")
123+
124+
let body = init?.body
125+
if (body) {
126+
try {
127+
let text: string
128+
if (typeof body === "string") {
129+
text = body
130+
} else if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
131+
text = new TextDecoder().decode(body)
132+
} else {
133+
// ReadableStream, Blob, FormData — pass through untransformed
134+
text = ""
135+
}
136+
if (text) {
137+
const result = transformSnowflakeBody(text)
138+
if (result.syntheticStop) return result.syntheticStop
139+
body = result.body
140+
headers.delete("content-length")
141+
}
142+
} catch {
143+
// JSON parse error — pass original body through untransformed
144+
}
145+
}
146+
147+
return fetch(requestInput, { ...init, headers, body })
148+
},
149+
}
150+
},
151+
methods: [
152+
{
153+
label: "Snowflake PAT",
154+
type: "oauth",
155+
authorize: async () => ({
156+
url: "https://app.snowflake.com",
157+
instructions:
158+
"Enter your credentials as: <account-identifier>::<PAT-token>\n e.g. myorg-myaccount::pat-token-here\n Create a PAT in Snowsight: Admin → Security → Programmatic Access Tokens",
159+
method: "code" as const,
160+
callback: async (code: string) => {
161+
const parsed = parseSnowflakePAT(code)
162+
if (!parsed) return { type: "failed" as const }
163+
return {
164+
type: "success" as const,
165+
access: parsed.token,
166+
refresh: "",
167+
// PATs have variable TTLs (default 90 days); use conservative expiry
168+
expires: Date.now() + 90 * 24 * 60 * 60 * 1000,
169+
accountId: parsed.account,
170+
}
171+
},
172+
}),
173+
},
174+
],
175+
},
176+
}
177+
}

packages/opencode/src/plugin/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import { Session } from "../session"
1212
import { NamedError } from "@opencode-ai/util/error"
1313
import { CopilotAuthPlugin } from "./copilot"
1414
import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-auth"
15+
// altimate_change start — snowflake cortex plugin import
16+
import { SnowflakeCortexAuthPlugin } from "../altimate/plugin/snowflake"
17+
// altimate_change end
1518

1619
export namespace Plugin {
1720
const log = Log.create({ service: "plugin" })
@@ -22,7 +25,9 @@ export namespace Plugin {
2225
// GitlabAuthPlugin uses a different version of @opencode-ai/plugin (from npm)
2326
// vs the workspace version, causing a type mismatch on internal HeyApiClient.
2427
// The types are structurally compatible at runtime.
25-
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin as unknown as PluginInstance]
28+
// altimate_change start — snowflake cortex internal plugin
29+
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin as unknown as PluginInstance, SnowflakeCortexAuthPlugin]
30+
// altimate_change end
2631

2732
const state = Instance.state(async () => {
2833
const client = createOpencodeClient({

packages/opencode/src/provider/provider.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ import { GoogleAuth } from "google-auth-library"
4646
import { ProviderTransform } from "./transform"
4747
import { Installation } from "../installation"
4848
import { ModelID, ProviderID } from "./schema"
49+
// altimate_change start — snowflake cortex account validation
50+
import { VALID_ACCOUNT_RE } from "../altimate/plugin/snowflake"
51+
// altimate_change end
4952

5053
const DEFAULT_CHUNK_TIMEOUT = 120_000
5154

@@ -670,6 +673,20 @@ export namespace Provider {
670673
},
671674
}
672675
},
676+
// altimate_change start — snowflake cortex provider loader
677+
"snowflake-cortex": async () => {
678+
const auth = await Auth.get("snowflake-cortex")
679+
if (auth?.type !== "oauth") return { autoload: false }
680+
const account = auth.accountId ?? Env.get("SNOWFLAKE_ACCOUNT")
681+
if (!account || !VALID_ACCOUNT_RE.test(account)) return { autoload: false }
682+
return {
683+
autoload: true,
684+
options: {
685+
baseURL: `https://${account}.snowflakecomputing.com/api/v2/cortex/v1`,
686+
},
687+
}
688+
},
689+
// altimate_change end
673690
}
674691

675692
export const Model = z
@@ -879,6 +896,83 @@ export namespace Provider {
879896
}
880897
}
881898

899+
// altimate_change start — snowflake cortex provider models
900+
function makeSnowflakeModel(
901+
id: string,
902+
name: string,
903+
limits: { context: number; output: number },
904+
caps?: { reasoning?: boolean; attachment?: boolean; toolcall?: boolean },
905+
): Model {
906+
const m: Model = {
907+
id: ModelID.make(id),
908+
providerID: ProviderID.snowflakeCortex,
909+
api: {
910+
id,
911+
url: "",
912+
npm: "@ai-sdk/openai-compatible",
913+
},
914+
name,
915+
capabilities: {
916+
temperature: true,
917+
reasoning: caps?.reasoning ?? false,
918+
attachment: caps?.attachment ?? false,
919+
toolcall: caps?.toolcall ?? true,
920+
input: { text: true, audio: false, image: false, video: false, pdf: false },
921+
output: { text: true, audio: false, image: false, video: false, pdf: false },
922+
interleaved: false,
923+
},
924+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
925+
limit: { context: limits.context, output: limits.output },
926+
status: "active" as const,
927+
options: {},
928+
headers: {},
929+
release_date: "2024-01-01",
930+
variants: {},
931+
}
932+
m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
933+
return m
934+
}
935+
936+
database["snowflake-cortex"] = {
937+
id: ProviderID.snowflakeCortex,
938+
source: "custom",
939+
name: "Snowflake Cortex",
940+
env: [],
941+
options: {},
942+
models: {
943+
// Claude models — tool calling supported
944+
"claude-sonnet-4-6": makeSnowflakeModel("claude-sonnet-4-6", "Claude Sonnet 4.6", { context: 200000, output: 64000 }),
945+
"claude-opus-4-6": makeSnowflakeModel("claude-opus-4-6", "Claude Opus 4.6", { context: 200000, output: 32000 }),
946+
"claude-sonnet-4-5": makeSnowflakeModel("claude-sonnet-4-5", "Claude Sonnet 4.5", { context: 200000, output: 64000 }),
947+
"claude-opus-4-5": makeSnowflakeModel("claude-opus-4-5", "Claude Opus 4.5", { context: 200000, output: 32000 }),
948+
"claude-haiku-4-5": makeSnowflakeModel("claude-haiku-4-5", "Claude Haiku 4.5", { context: 200000, output: 16000 }),
949+
"claude-4-sonnet": makeSnowflakeModel("claude-4-sonnet", "Claude 4 Sonnet", { context: 200000, output: 64000 }),
950+
// claude-4-opus: documented but gated (403 "account not allowed" on tested accounts)
951+
"claude-3-7-sonnet": makeSnowflakeModel("claude-3-7-sonnet", "Claude 3.7 Sonnet", { context: 200000, output: 16000 }),
952+
"claude-3-5-sonnet": makeSnowflakeModel("claude-3-5-sonnet", "Claude 3.5 Sonnet", { context: 200000, output: 8192 }),
953+
// OpenAI models — tool calling supported
954+
"openai-gpt-4.1": makeSnowflakeModel("openai-gpt-4.1", "OpenAI GPT-4.1", { context: 1047576, output: 32768 }),
955+
"openai-gpt-5": makeSnowflakeModel("openai-gpt-5", "OpenAI GPT-5", { context: 1047576, output: 32768 }),
956+
"openai-gpt-5-mini": makeSnowflakeModel("openai-gpt-5-mini", "OpenAI GPT-5 Mini", { context: 1047576, output: 32768 }),
957+
"openai-gpt-5-nano": makeSnowflakeModel("openai-gpt-5-nano", "OpenAI GPT-5 Nano", { context: 1047576, output: 32768 }),
958+
"openai-gpt-5-chat": makeSnowflakeModel("openai-gpt-5-chat", "OpenAI GPT-5 Chat", { context: 1047576, output: 32768 }),
959+
// openai-gpt-oss-120b: documented but returns 500 (not yet stable)
960+
// Meta Llama — no tool calling
961+
"llama4-maverick": makeSnowflakeModel("llama4-maverick", "Llama 4 Maverick", { context: 1048576, output: 4096 }, { toolcall: false }),
962+
"snowflake-llama-3.3-70b": makeSnowflakeModel("snowflake-llama-3.3-70b", "Snowflake Llama 3.3 70B", { context: 128000, output: 4096 }, { toolcall: false }),
963+
"llama3.1-70b": makeSnowflakeModel("llama3.1-70b", "Llama 3.1 70B", { context: 128000, output: 4096 }, { toolcall: false }),
964+
"llama3.1-405b": makeSnowflakeModel("llama3.1-405b", "Llama 3.1 405B", { context: 128000, output: 4096 }, { toolcall: false }),
965+
"llama3.1-8b": makeSnowflakeModel("llama3.1-8b", "Llama 3.1 8B", { context: 128000, output: 4096 }, { toolcall: false }),
966+
// Mistral — no tool calling
967+
"mistral-large": makeSnowflakeModel("mistral-large", "Mistral Large", { context: 131000, output: 4096 }, { toolcall: false }),
968+
"mistral-large2": makeSnowflakeModel("mistral-large2", "Mistral Large 2", { context: 131000, output: 4096 }, { toolcall: false }),
969+
"mistral-7b": makeSnowflakeModel("mistral-7b", "Mistral 7B", { context: 32000, output: 4096 }, { toolcall: false }),
970+
// DeepSeek — no tool calling
971+
"deepseek-r1": makeSnowflakeModel("deepseek-r1", "DeepSeek R1", { context: 64000, output: 32000 }, { reasoning: true, toolcall: false }),
972+
},
973+
}
974+
// altimate_change end
975+
882976
function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
883977
const existing = providers[providerID]
884978
if (existing) {

packages/opencode/src/provider/schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export const ProviderID = providerIdSchema.pipe(
2323
azure: schema.makeUnsafe("azure"),
2424
openrouter: schema.makeUnsafe("openrouter"),
2525
mistral: schema.makeUnsafe("mistral"),
26+
// altimate_change start — snowflake cortex provider ID
27+
snowflakeCortex: schema.makeUnsafe("snowflake-cortex"),
28+
// altimate_change end
2629
})),
2730
)
2831

0 commit comments

Comments
 (0)