Compare commits

...
2 changed files with 464 additions and 328 deletions
@@ -1,90 +1,272 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Form } from "@opencode-ai/schema/form"
import { Clock, Deferred, Effect, Option, Schema, Stream } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { App } from "../../app.js"
import { Bus } from "../../bus.js"
import { Credential } from "../../credential.js"
import { Integration } from "../../integration.js"
import { OauthCallbackPage } from "../../oauth/page.js"
import { Provider } from "../../provider.js"
import { configuredSettings } from "./configured.js"
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
// Exported for testing: intercepts Cortex-specific request/response quirks.
export function cortexFetch(upstream: FetchLike = fetch) {
return async (url: string | URL | Request, init?: RequestInit): Promise<Response> => {
if (init?.body && typeof init.body === "string") {
try {
const body = JSON.parse(init.body)
if ("max_tokens" in body) {
body.max_completion_tokens = body.max_tokens
delete body.max_tokens
init = { ...init, body: JSON.stringify(body) }
}
} catch {}
}
const response = await upstream(url, init)
// Cortex returns 400 "conversation complete" as a normal stop condition
if (response.status === 400) {
try {
const errorData = (await response.clone().json()) as Record<string, unknown>
if (
String(errorData.message || errorData.error || "")
.toLowerCase()
.includes("conversation complete")
) {
return new Response(
JSON.stringify({ choices: [{ finish_reason: "stop", message: { content: "", role: "assistant" } }] }),
{ status: 200, headers: new Headers({ "content-type": "application/json" }) },
)
}
} catch {}
}
// Cortex returns role:"" in streaming deltas; the AI SDK schema requires "assistant"
if (response.body && response.headers.get("content-type")?.includes("text/event-stream")) {
const reader = response.body.getReader()
const encoder = new TextEncoder()
const decoder = new TextDecoder()
const stream = new ReadableStream({
async pull(ctrl) {
const { done, value } = await reader.read()
if (done) {
ctrl.close()
return
}
ctrl.enqueue(
encoder.encode(decoder.decode(value, { stream: true }).replace(/"role"\s*:\s*""/g, '"role":"assistant"')),
)
},
cancel() {
reader.cancel()
},
})
return new Response(stream, { headers: response.headers, status: response.status })
}
return response
}
}
const providerID = Provider.ID.make("snowflake-cortex")
const integrationID = Integration.ID.make(providerID)
const methodID = Integration.MethodID.make("browser")
const clientID = "LOCAL_APPLICATION"
const accountForm = Form.Fields.make([
{
type: "string",
key: "account",
title: "Snowflake account",
placeholder: "myorg-myaccount",
required: true,
},
])
const Token = Schema.Struct({
access_token: Schema.NonEmptyString,
refresh_token: Schema.optional(Schema.String),
expires_in: Schema.optional(Schema.Number),
})
const decodeError = Schema.decodeUnknownOption(
Schema.fromJsonString(
Schema.Struct({ message: Schema.optional(Schema.Unknown), error: Schema.optional(Schema.Unknown) }),
),
)
export const SnowflakeCortexPlugin = define({
id: "opencode.provider.snowflake.cortex",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID !== Provider.ID.make("snowflake-cortex")) return
const token =
process.env.SNOWFLAKE_CORTEX_TOKEN ??
process.env.SNOWFLAKE_CORTEX_PAT ??
(typeof evt.options.token === "string" ? evt.options.token : undefined) ??
(typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined)
const upstream = typeof evt.options.fetch === "function" ? (evt.options.fetch as FetchLike) : undefined
if (evt.options.includeUsage !== false) evt.options.includeUsage = true
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
evt.sdk = mod.createOpenAICompatible({
...evt.options,
...(token ? { apiKey: token } : {}),
fetch: cortexFetch(upstream) as typeof fetch,
} as any)
const http = yield* HttpClient.HttpClient
const credentials = yield* Credential.Service
const integrations = yield* Integration.Service
const bus = yield* Bus.Service
const configured = yield* configuredSettings(providerID)
const account = { value: "" }
const saved = Effect.gen(function* () {
const connection = yield* integrations.connection.active(integrationID)
return connection?.type === "credential" ? yield* credentials.get(connection.id) : undefined
})
const token = Effect.fn(function* (account: string, form: Record<string, string>, current?: string) {
if (!account) return yield* Effect.fail(new Error("Snowflake account is required"))
const response = yield* http.execute(
HttpClientRequest.post(`${issuer(account)}/oauth/token-request`).pipe(
HttpClientRequest.setHeaders({
Accept: "application/json",
"User-Agent": App.useragent(ctx.app),
// Same built-in OAuth client and Basic header as V1; this is not a user password.
Authorization: `Basic ${Buffer.from(`${clientID}:${clientID}`).toString("base64")}`,
}),
HttpClientRequest.bodyUrlParams({ ...form, client_id: clientID }),
),
)
if (response.status < 200 || response.status >= 300)
return yield* Effect.fail(
new Error(`Snowflake token request failed (${response.status}): ${yield* response.text}`),
)
const tokens = yield* HttpClientResponse.schemaBodyJson(Token)(response)
const refresh = tokens.refresh_token || current
if (!refresh) return yield* Effect.fail(new Error("Snowflake token response did not include refresh_token"))
return Credential.OAuth.make({
type: "oauth",
methodID,
access: tokens.access_token,
refresh,
expires: (yield* Clock.currentTimeMillis) + (tokens.expires_in ?? 600) * 1000,
metadata: { account },
})
})
const refresh = (value: Credential.OAuth) =>
token(
normalizeAccount(value.metadata?.account),
{ grant_type: "refresh_token", refresh_token: value.refresh },
value.refresh,
)
yield* ctx.integration.transform((editor) => {
editor.method.update({
integrationID,
method: { type: "key", label: "Paste PAT or bearer token", form: accountForm },
})
// SNOWFLAKE_ACCOUNT configures the endpoint; it is not a token.
editor.method.update({
integrationID,
method: { type: "env", names: ["SNOWFLAKE_CORTEX_TOKEN", "SNOWFLAKE_CORTEX_PAT"] },
})
editor.method.update({
integrationID,
method: {
id: methodID,
type: "oauth",
label: "Login with Snowflake (External Browser)",
form: [...accountForm, { type: "string", key: "role", title: "Snowflake role (optional)" }],
},
refresh,
label: (value) => normalizeAccount(value.metadata?.account),
authorize: (answer) =>
Effect.gen(function* () {
const account = normalizeAccount(answer.account)
if (!account) return yield* Effect.fail(new Error("Snowflake account is required"))
const role = typeof answer.role === "string" ? answer.role.trim() : ""
const verifier = Buffer.from(crypto.getRandomValues(new Uint8Array(48))).toString("base64url")
const challenge = Buffer.from(
yield* Effect.promise(() => crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))),
).toString("base64url")
const state = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url")
const code = yield* Deferred.make<string, Error>()
const { createServer } = yield* Effect.promise(() => import("node:http"))
const { EventEmitter } = yield* Effect.promise(() => import("node:events"))
const server = createServer((request, response) => {
const url = new URL(request.url ?? "/", "http://127.0.0.1")
if (url.pathname !== "/") {
response.writeHead(404).end()
return
}
const value = url.searchParams.get("code")
const error =
url.searchParams.get("state") !== state
? "Invalid OAuth state"
: url.searchParams.get("error_description") ||
url.searchParams.get("error") ||
(!value ? "Missing authorization code" : undefined)
Effect.runFork(error ? Deferred.fail(code, new Error(error)) : Deferred.succeed(code, value ?? ""))
response
.writeHead(error ? 400 : 200, { "Content-Type": "text/html" })
.end(
error
? OauthCallbackPage.error(error, { provider: "Snowflake" })
: OauthCallbackPage.success({ provider: "Snowflake" }),
)
})
yield* Effect.addFinalizer(() => Effect.sync(() => server.close()))
yield* Effect.tryPromise(() => EventEmitter.once(server.listen(0, "127.0.0.1"), "listening"))
const address = server.address()
if (!address || typeof address === "string")
return yield* Effect.fail(new Error("Missing OAuth callback port"))
const redirect = `http://127.0.0.1:${address.port}/`
return {
mode: "auto" as const,
url: `${issuer(account)}/oauth/authorize?${new URLSearchParams({
client_id: clientID,
response_type: "code",
redirect_uri: redirect,
state,
scope: !role
? "refresh_token"
: /^[-_A-Za-z0-9]+$/.test(role)
? `refresh_token session:role:${role}`
: `refresh_token session:role-encoded:${encodeURIComponent(role)}`,
code_challenge: challenge,
code_challenge_method: "S256",
}).toString()}`,
instructions: "Complete Snowflake sign-in in your browser.",
callback: Deferred.await(code).pipe(
Effect.flatMap((code) =>
token(account, {
grant_type: "authorization_code",
code,
redirect_uri: redirect,
code_verifier: verifier,
}),
),
),
}
}),
})
})
const load = Effect.gen(function* () {
const value = (yield* saved)?.value
account.value = normalizeAccount(
process.env.SNOWFLAKE_ACCOUNT ??
(value?.type === "key"
? (value.configuration?.account ?? value.metadata?.account)
: value?.metadata?.account) ??
configured?.account,
)
})
yield* load
yield* ctx.catalog.transform((catalog) => {
const item = catalog.provider.get(providerID)
if (!item) return
const settings = { ...item.provider.settings, ...configured }
item.provider.package = "@opencode-ai/ai/providers/openai-compatible"
item.provider.settings = {
...settings,
provider: providerID,
baseURL: endpoint(settings.baseURL, account.value),
...(typeof settings.token === "string" ? { apiKey: settings.token } : {}),
}
for (const model of item.models.values()) {
model.package = item.provider.package
model.compatibility = { maxTokensField: "max_completion_tokens", ...model.compatibility }
if (model.settings?.baseURL !== undefined)
model.settings.baseURL = endpoint(model.settings.baseURL, account.value)
}
})
yield* bus.subscribe(Credential.Event.Switched).pipe(
Stream.filter((event) => event.data.integrationID === integrationID),
Stream.runForEach(() => load.pipe(Effect.andThen(ctx.catalog.reload()))),
Effect.forkScoped({ startImmediately: true }),
)
yield* ctx.session.hook(
"http.request",
(event) =>
Effect.sync(() => {
// Model resolution already supplies and refreshes stored credentials on each attempt.
const token = envToken()
if (token) event.request.headers.set("authorization", `Bearer ${token}`)
event.request.headers.set("user-agent", App.useragent(ctx.app))
}),
{ providerID },
)
yield* ctx.session.hook(
"http.response",
Effect.fn(function* (event) {
if (event.response.status !== 400) return
const error = Option.getOrUndefined(decodeError(yield* Effect.promise(() => event.response.clone().text())))
// oxlint-disable-next-line typescript-eslint/no-base-to-string -- Preserve V1's error-body coercion.
const message = String(error?.message || error?.error || "")
if (!message.toLowerCase().includes("conversation complete")) return
event.response = new Response(
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n',
{ headers: { "content-type": "text/event-stream" } },
)
}),
{ providerID },
)
yield* ctx.session.hook(
"retry",
Effect.fn(function* (event) {
if (event.error.status !== 401 || event.attempt !== 2 || envToken()) return
const current = yield* saved
if (current?.value.type !== "oauth" || current.value.methodID !== methodID) return
yield* credentials.update(current.id, { value: yield* refresh(current.value).pipe(Effect.orDie) })
event.decision = { retry: true, delay: 0 }
}),
{ providerID },
)
}),
})
function normalizeAccount(value: unknown) {
if (typeof value !== "string") return ""
return value
.trim()
.replace(/^https?:\/\//i, "")
.replace(/\/+$/, "")
.replace(/\.snowflakecomputing\.com$/i, "")
}
function issuer(account: string) {
return `https://${account}.snowflakecomputing.com`
}
function endpoint(value: unknown, account: string) {
const baseURL = typeof value === "string" ? value : `${issuer("${SNOWFLAKE_ACCOUNT}")}/api/v2/cortex/v1`
return account ? baseURL.replaceAll("${SNOWFLAKE_ACCOUNT}", account) : baseURL
}
function envToken() {
return process.env.SNOWFLAKE_CORTEX_TOKEN ?? process.env.SNOWFLAKE_CORTEX_PAT
}
@@ -1,262 +1,216 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { Message } from "@opencode-ai/ai"
import { LLMClient, RequestExecutor } from "@opencode-ai/ai/route"
import { Agent } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { Location } from "@opencode-ai/core/location"
import { Model } from "@opencode-ai/core/model"
import { ModelResolver } from "@opencode-ai/core/model-resolver"
import { Plugin } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { SnowflakeCortexPlugin, cortexFetch } from "@opencode-ai/core/plugin/provider/snowflake-cortex"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
import { SnowflakeCortexPlugin } from "@opencode-ai/core/plugin/provider/snowflake-cortex"
import { Provider } from "@opencode-ai/core/provider"
import { Session } from "@opencode-ai/core/session"
import { SessionModelRequest } from "@opencode-ai/core/session/model-request"
import { SessionModelTransport } from "@opencode-ai/core/session/model-transport"
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { expect } from "bun:test"
import { Effect, Layer, Schedule, Stream } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { Socket } from "effect/unstable/socket"
import { withEnv } from "../fixture/env"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
const it = testEffect(
Layer.merge(PluginTestLayer, SessionModelTransport.layer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal))),
)
const providerID = Provider.ID.make("snowflake-cortex")
const integrationID = Integration.ID.make(providerID)
const methodID = Integration.MethodID.make("browser")
const modelID = Model.ID.make("claude-sonnet-4-6")
const env = { SNOWFLAKE_ACCOUNT: undefined, SNOWFLAKE_CORTEX_TOKEN: undefined, SNOWFLAKE_CORTEX_PAT: undefined }
const endpoint = "https://myorg-myaccount.snowflakecomputing.com"
const addPlugin = Effect.fn(function* () {
const fixture = Effect.fn(function* () {
const requests: Request[] = []
const replies: Response[] = []
const http = HttpClient.make((request) =>
Effect.gen(function* () {
requests.push(yield* HttpClientRequest.toWeb(request).pipe(Effect.orDie))
const response = replies.shift()
if (!response) throw new Error(`Unexpected request: ${request.url}`)
return HttpClientResponse.fromWeb(request, response)
}),
)
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
const sessions = yield* Session.Service
const location = yield* Location.Service
const hooks = yield* PluginHooks.Service
const plugin = yield* Plugin.Service
const host = yield* PluginHost.make(plugin)
yield* SnowflakeCortexPlugin.effect(host)
})
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease(
Effect.sync(() => {
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
Object.entries(vars).forEach(([key, value]) => {
if (value === undefined) delete process.env[key]
else process.env[key] = value
})
return previous
}),
effect,
(previous) =>
Effect.sync(() => {
Object.entries(previous).forEach(([key, value]) => {
if (value === undefined) delete process.env[key]
else process.env[key] = value
})
}),
)
}
describe("SnowflakeCortexPlugin", () => {
it.effect("is registered in ProviderPlugins before OpenAICompatiblePlugin", () =>
Effect.sync(() => {
expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.snowflake.cortex")
const ids = ProviderPlugins.map((p) => p.id)
expect(ids.indexOf("opencode.provider.snowflake.cortex")).toBeLessThan(
ids.indexOf("opencode.provider.openai.compatible"),
)
}),
)
it.effect("ignores non-snowflake-cortex providers", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: Model.Info.make({
...Model.Info.default(Provider.ID.make("openai"), Model.ID.make("gpt-4")),
modelID: Model.ID.make("gpt-4"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/openai",
options: { name: "openai" },
})
expect(result.sdk).toBeUndefined()
}),
)
it.effect("creates SDK for snowflake-cortex using SNOWFLAKE_CORTEX_PAT env var", () =>
withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: Model.Info.make({
...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")),
modelID: Model.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
})
expect(result.sdk).toBeDefined()
}),
),
)
it.effect("falls back to options.apiKey when SNOWFLAKE_CORTEX_PAT env var is absent", () =>
withEnv({ SNOWFLAKE_CORTEX_PAT: undefined }, () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: Model.Info.make({
...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")),
modelID: Model.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: {
name: "snowflake-cortex",
baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1",
apiKey: "options-pat",
},
})
expect(result.sdk).toBeDefined()
}),
),
)
it.effect("uses SNOWFLAKE_CORTEX_TOKEN env var", () =>
withEnv({ SNOWFLAKE_CORTEX_TOKEN: "oauth-token", SNOWFLAKE_CORTEX_PAT: undefined }, () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: Model.Info.make({
...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")),
modelID: Model.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
})
expect(result.sdk).toBeDefined()
}),
),
)
it.effect("falls back to options.token when no Snowflake env token is set", () =>
withEnv({ SNOWFLAKE_CORTEX_TOKEN: undefined, SNOWFLAKE_CORTEX_PAT: undefined }, () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: Model.Info.make({
...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")),
modelID: Model.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: {
name: "snowflake-cortex",
baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1",
token: "options-token",
},
})
expect(result.sdk).toBeDefined()
}),
),
)
it.effect("sets includeUsage on the SDK options", () =>
withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
yield* addPlugin()
const result = yield* aisdk.runSDK({
model: Model.Info.make({
...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")),
modelID: Model.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
})
expect(result.options.includeUsage).toBe(true)
}),
),
)
})
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
describe("cortexFetch", () => {
test("rewrites max_tokens to max_completion_tokens", async () => {
const captured: RequestInit[] = []
const upstream: FetchLike = async (_url, init) => {
captured.push(init ?? {})
return new Response("{}", { status: 200 })
}
await cortexFetch(upstream)("https://test", {
method: "POST",
body: JSON.stringify({ model: "claude-sonnet-4-6", max_tokens: 1024 }),
yield* catalog.transform((editor) => {
editor.provider.update(providerID, (provider) => {
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://${SNOWFLAKE_ACCOUNT}.snowflakecomputing.com/api/v2/cortex/v1" }
})
const body = JSON.parse(captured[0].body as string)
expect(body.max_completion_tokens).toBe(1024)
expect(body.max_tokens).toBeUndefined()
})
test("preserves body when max_tokens is absent", async () => {
const captured: RequestInit[] = []
const upstream: FetchLike = async (_url, init) => {
captured.push(init ?? {})
return new Response("{}", { status: 200 })
}
const original = JSON.stringify({ model: "claude-sonnet-4-6", temperature: 0.7 })
await cortexFetch(upstream)("https://test", { method: "POST", body: original })
expect(captured[0].body).toBe(original)
})
test("treats 400 'conversation complete' as a stop response", async () => {
const upstream: FetchLike = async () =>
new Response(JSON.stringify({ message: "Conversation complete" }), {
status: 400,
headers: { "content-type": "application/json" },
})
const response = await cortexFetch(upstream)("https://test", {})
expect(response.status).toBe(200)
const data = (await response.json()) as { choices: { finish_reason: string }[] }
expect(data.choices[0].finish_reason).toBe("stop")
})
test("passes through other 400 errors unchanged", async () => {
const upstream: FetchLike = async () =>
new Response(JSON.stringify({ message: "Invalid model" }), {
status: 400,
headers: { "content-type": "application/json" },
})
const response = await cortexFetch(upstream)("https://test", {})
expect(response.status).toBe(400)
})
test("passes through non-400 errors unchanged", async () => {
const upstream: FetchLike = async () => new Response("Unauthorized", { status: 401 })
const response = await cortexFetch(upstream)("https://test", {})
expect(response.status).toBe(401)
})
test("handles invalid JSON body gracefully without throwing", async () => {
const captured: RequestInit[] = []
const upstream: FetchLike = async (_url, init) => {
captured.push(init ?? {})
return new Response("{}", { status: 200 })
}
const invalidBody = "{ not json }"
await cortexFetch(upstream)("https://test", { method: "POST", body: invalidBody })
expect(captured[0].body).toBe(invalidBody)
})
test("rewrites role:'' to role:'assistant' in streaming SSE chunks", async () => {
const chunk = `data: {"choices":[{"delta":{"role":"","content":"Hi"},"index":0}]}\n\n`
const upstream: FetchLike = async () =>
new Response(
new ReadableStream({
start: (ctrl) => {
ctrl.enqueue(new TextEncoder().encode(chunk))
ctrl.close()
},
}),
{
status: 200,
headers: { "content-type": "text/event-stream" },
},
)
const response = await cortexFetch(upstream)("https://test", {})
const text = await response.text()
expect(text).toContain('"role":"assistant"')
expect(text).not.toContain('"role":""')
editor.model.update(providerID, modelID, () => {})
})
yield* SnowflakeCortexPlugin.effect(host).pipe(Effect.provideService(HttpClient.HttpClient, http))
const session = yield* sessions.create({ location: Location.Ref.make({ directory: location.directory }) })
const scope = {
sessionID: session.id,
agent: Agent.ID.make("build"),
model: Model.Ref.make({ providerID, id: modelID }),
}
const send = Effect.gen(function* () {
const connection = yield* integrations.connection.active(integrationID)
const credential = connection ? yield* integrations.connection.resolve(connection) : undefined
const model = yield* Effect.gen(function* () {
const model = yield* catalog.model.get(providerID, modelID)
if (!model || String(model.settings?.baseURL).includes("${")) return yield* Effect.fail("Catalog pending")
return model
}).pipe(Effect.retry({ times: 100, schedule: Schedule.spaced("1 millis") }))
const resolved = yield* ModelResolver.fromCatalogModel(model, credential)
const service = yield* SessionModelRequest.Service
const prepared = yield* service.prepare({
scope: { session, agentID: scope.agent, model: SessionRunnerModel.resolved(resolved, model) },
transcript: { system: [], messages: [Message.user("Hello")] },
})
return yield* LLMClient.stream(prepared.request, prepared.options).pipe(
Stream.runCollect,
Effect.provide(LLMClient.layer.pipe(Layer.provide(RequestExecutor.layer), Layer.fresh)),
Effect.provideService(HttpClient.HttpClient, http),
)
}).pipe(Effect.provide(SessionModelRequest.layer))
return { requests, replies, integrations, hooks, scope, send }
})
const status = Effect.fn(function* (attemptID: Integration.AttemptID) {
const integrations = yield* Integration.Service
return yield* integrations.oauth.status({ integrationID, attemptID }).pipe(
Effect.filterOrFail(
(status) => status.status !== "pending",
() => "OAuth pending",
),
Effect.retry({ times: 100, schedule: Schedule.spaced("1 millis") }),
)
})
it.live("browser OAuth supplies the native endpoint/token and refreshes a rejected token once", () =>
withEnv(env, () =>
Effect.gen(function* () {
const test = yield* fixture()
const attempt = yield* test.integrations.oauth.connect({
integrationID,
methodID,
answer: { account: `${endpoint}///`, role: "My Role" },
})
const url = new URL(attempt.url)
expect(url.origin + url.pathname).toBe(`${endpoint}/oauth/authorize`)
expect(url.searchParams.get("scope")).toBe("refresh_token session:role-encoded:My%20Role")
const callback = new URL(url.searchParams.get("redirect_uri") ?? "")
callback.searchParams.set("state", url.searchParams.get("state") ?? "")
callback.searchParams.set("code", "auth-code")
test.replies.push(Response.json({ access_token: "access", refresh_token: "refresh", expires_in: 3600 }))
yield* Effect.promise(() => fetch(callback))
expect((yield* status(attempt.attemptID)).status).toBe("complete")
const exchange = test.requests[0]
expect(exchange.url).toBe(`${endpoint}/oauth/token-request`)
expect(exchange.headers.get("authorization")).toBe(
`Basic ${Buffer.from("LOCAL_APPLICATION:LOCAL_APPLICATION").toString("base64")}`,
)
const form = new URLSearchParams(yield* Effect.promise(() => exchange.text()))
expect(Object.fromEntries(form)).toMatchObject({
grant_type: "authorization_code",
code: "auth-code",
redirect_uri: url.searchParams.get("redirect_uri"),
})
const challenge = Buffer.from(
yield* Effect.promise(() =>
crypto.subtle.digest("SHA-256", new TextEncoder().encode(form.get("code_verifier") ?? "")),
),
).toString("base64url")
expect(url.searchParams.get("code_challenge")).toBe(challenge)
test.replies.push(new Response("Unauthorized", { status: 401 }))
expect((yield* test.send.pipe(Effect.exit))._tag).toBe("Failure")
expect(test.requests[1].headers.get("authorization")).toBe("Bearer access")
test.replies.push(Response.json({ access_token: "renewed", refresh_token: "", expires_in: 3600 }))
const retry = {
...test.scope,
error: { type: "provider.authentication", message: "Unauthorized", status: 401 },
attempt: 2,
decision: { retry: false as const },
}
expect((yield* test.hooks.trigger("session", "retry", retry)).decision).toEqual({ retry: true, delay: 0 })
const refresh = new URLSearchParams(yield* Effect.promise(() => test.requests[2].text()))
expect(Object.fromEntries(refresh)).toMatchObject({ grant_type: "refresh_token", refresh_token: "refresh" })
test.replies.push(Response.json({ message: "Conversation complete", error: {} }, { status: 400 }))
expect((yield* test.send).find((event) => event.type === "finish")?.reason.normalized).toBe("stop")
expect(test.requests[3].headers.get("authorization")).toBe("Bearer renewed")
expect(
(yield* test.hooks.trigger("session", "retry", { ...retry, attempt: 3, decision: { retry: false } })).decision
.retry,
).toBe(false)
const credentials = yield* Credential.Service
expect((yield* credentials.list(integrationID))[0]?.value).toMatchObject({
access: "renewed",
refresh: "refresh",
})
}),
),
)
it.live("manual PAT uses its account and native compatibility rather than an SDK request rewrite", () =>
withEnv(env, () =>
Effect.gen(function* () {
const test = yield* fixture()
yield* test.integrations.connection.key({ integrationID, key: "pat", answer: { account: `${endpoint}/` } })
yield* test.hooks.register(
"session",
"context",
(event) =>
Effect.sync(() => {
event.generation.maxTokens = 1024
}),
{ providerID },
)
test.replies.push(
new Response(
'data: {"choices":[{"index":0,"delta":{"role":"","content":"Hello"},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n',
{ headers: { "content-type": "text/event-stream" } },
),
)
expect(yield* test.send).toContainEqual(expect.objectContaining({ type: "text-delta", text: "Hello" }))
const request = test.requests[0]
expect(request.url).toBe(`${endpoint}/api/v2/cortex/v1/chat/completions`)
expect(request.headers.get("authorization")).toBe("Bearer pat")
const body = yield* Effect.promise(() => request.json())
expect(body).toMatchObject({ model: modelID, max_completion_tokens: 1024 })
expect(body).not.toHaveProperty("max_tokens")
}),
),
)
it.live("rejects a mismatched callback state before token exchange", () =>
withEnv(env, () =>
Effect.gen(function* () {
const test = yield* fixture()
const attempt = yield* test.integrations.oauth.connect({
integrationID,
methodID,
answer: { account: "myorg-myaccount" },
})
const callback = new URL(new URL(attempt.url).searchParams.get("redirect_uri") ?? "")
callback.searchParams.set("state", "wrong")
callback.searchParams.set("code", "forged")
expect((yield* Effect.promise(() => fetch(callback))).status).toBe(400)
expect(yield* status(attempt.attemptID)).toMatchObject({ status: "failed", message: "Invalid OAuth state" })
expect(test.requests).toHaveLength(0)
}),
),
)