diff --git a/packages/server/clients/cacheClient.ts b/packages/server/clients/cacheClient.ts index e5ec16382..2ce0c4878 100644 --- a/packages/server/clients/cacheClient.ts +++ b/packages/server/clients/cacheClient.ts @@ -1,6 +1,5 @@ import type { Protocol } from "devtools-protocol"; import { z } from "zod/v4"; -import type { BrowserbaseRegion } from "../../protocol/types.js"; /** * HTTP client for the Stagehand API's stateless cache routes (POST /cache/get @@ -12,22 +11,6 @@ import type { BrowserbaseRegion } from "../../protocol/types.js"; * server-side contract. */ -/** Stagehand API base URL per Browserbase region; sessions must hit the API - * deployment in the region their browser runs in. Mirrors the v3 SDK's - * REGION_API_URLS. */ -const REGION_API_URLS: Record = { - "us-west-2": "https://api.stagehand.browserbase.com", - "us-east-1": "https://api.use1.stagehand.browserbase.com", - "eu-central-1": "https://api.euc1.stagehand.browserbase.com", - "ap-southeast-1": "https://api.apse1.stagehand.browserbase.com", -}; - -const DEFAULT_REGION: BrowserbaseRegion = "us-west-2"; - -export function apiUrlForRegion(region: BrowserbaseRegion | undefined): string { - return `${REGION_API_URLS[region ?? DEFAULT_REGION]}/v1`; -} - /** Ceiling for a single cache HTTP round-trip so a slow cache can only ever * add a bounded delay to the action it fronts. */ const CACHE_REQUEST_TIMEOUT_MS = 5_000; diff --git a/packages/server/clients/stagehandApi.ts b/packages/server/clients/stagehandApi.ts new file mode 100644 index 000000000..1cdcc48dd --- /dev/null +++ b/packages/server/clients/stagehandApi.ts @@ -0,0 +1,16 @@ +import type { BrowserbaseRegion } from "../../protocol/types.js"; + +/** Stagehand API base URL per Browserbase region. */ +const REGION_API_URLS: Record = { + "us-west-2": "https://api.stagehand.browserbase.com", + "us-east-1": "https://api.use1.stagehand.browserbase.com", + "eu-central-1": "https://api.euc1.stagehand.browserbase.com", + "ap-southeast-1": "https://api.apse1.stagehand.browserbase.com", +}; + +const DEFAULT_REGION: BrowserbaseRegion = "us-west-2"; + +/** Resolves the regional Stagehand API endpoint for a Browserbase session. */ +export function apiUrlForRegion(region: BrowserbaseRegion | undefined): string { + return `${REGION_API_URLS[region ?? DEFAULT_REGION]}/v1`; +} diff --git a/packages/server/controllers/stagehandController.ts b/packages/server/controllers/stagehandController.ts index 543ee41a5..447e4146a 100644 --- a/packages/server/controllers/stagehandController.ts +++ b/packages/server/controllers/stagehandController.ts @@ -10,6 +10,7 @@ import type { StagehandRuntime } from "../runtime.js"; import * as actService from "../services/actService.js"; import * as cacheService from "../services/cacheService.js"; import * as extractService from "../services/extractService.js"; +import { buildGatewayContext } from "../llm/gatewayClient.js"; import * as observeService from "../services/observeService.js"; export function createStagehandController(runtime: StagehandRuntime) { @@ -46,6 +47,7 @@ export function createStagehandController(runtime: StagehandRuntime) { selfHeal: state.initParams.selfHeal, domSettleTimeoutMs: state.initParams.domSettleTimeoutMs, cache: cacheService.buildCacheContext(state.initParams), + gateway: buildGatewayContext(state.initParams), }); } @@ -69,6 +71,7 @@ export function createStagehandController(runtime: StagehandRuntime) { logger, systemPrompt: state.initParams.systemPrompt, cache: cacheService.buildCacheContext(state.initParams), + gateway: buildGatewayContext(state.initParams), }); } @@ -92,6 +95,7 @@ export function createStagehandController(runtime: StagehandRuntime) { logger, systemPrompt: state.initParams.systemPrompt, cache: cacheService.buildCacheContext(state.initParams), + gateway: buildGatewayContext(state.initParams), }); } diff --git a/packages/server/llm/aiSdkClient.ts b/packages/server/llm/aiSdkClient.ts index 5c3dfd392..0e93d7512 100644 --- a/packages/server/llm/aiSdkClient.ts +++ b/packages/server/llm/aiSdkClient.ts @@ -139,7 +139,7 @@ export function createAiSdkLanguageModel(config: KnownModelConfig): LanguageMode switch (provider) { case "openai": - return createOpenAI(connection).responses(OpenAIModelIdSchema.parse(modelId)); + return createOpenAI(connection).chat(OpenAIModelIdSchema.parse(modelId)); case "anthropic": return createAnthropic(connection)(AnthropicModelIdSchema.parse(modelId)); case "google": diff --git a/packages/server/llm/gatewayClient.ts b/packages/server/llm/gatewayClient.ts new file mode 100644 index 000000000..a6d7d6ffa --- /dev/null +++ b/packages/server/llm/gatewayClient.ts @@ -0,0 +1,55 @@ +import { createOpenAI } from "@ai-sdk/openai"; +import type { LanguageModel } from "ai"; +import type { KnownModelConfig, StagehandInitParams } from "../../protocol/types.js"; +import { apiUrlForRegion } from "../clients/stagehandApi.js"; + +/** + * Browserbase model-gateway inference over the Stagehand API's + * OpenAI Responses API endpoint (POST /v1/llm/responses). Used for runs with + * no customer model API key: the server authenticates the Browserbase API key + * against the session, routes the model through its gateway allowlist, and + * forwards the request to the AI gateway with server-side credentials. The + * whole Responses wire mapping is owned by @ai-sdk/openai's responses model; + * this module only supplies the endpoint and auth headers. + */ + +export interface GatewayContext { + apiUrl: string; + apiKey: string; + sessionId: string; +} + +/** + * Builds the per-instance gateway context from init params. Returns undefined + * when gateway inference is impossible: like the stateless cache routes, the + * endpoint authenticates with the Browserbase API key and resolves the project + * from the session id, so both are required. + */ +export function buildGatewayContext(initParams: StagehandInitParams): GatewayContext | undefined { + const sessionId = initParams.browser?.sessionId; + if (!initParams.apiKey || !sessionId) return undefined; + return { + apiUrl: apiUrlForRegion(initParams.browser?.region), + apiKey: initParams.apiKey, + sessionId, + }; +} + +/** Creates an AI SDK model that runs through the Browserbase model gateway. */ +export function createGatewayLanguageModel( + config: KnownModelConfig, + gateway: GatewayContext, +): LanguageModel { + const provider = createOpenAI({ + // Satisfies the SDK's required-key check; the server authenticates via + // the x-bb-* headers and ignores the Authorization header. + apiKey: gateway.apiKey, + baseURL: `${gateway.apiUrl}/llm`, + headers: { + "x-bb-api-key": gateway.apiKey, + "x-bb-session-id": gateway.sessionId, + ...config.headers, + }, + }); + return provider.responses(config.modelName); +} diff --git a/packages/server/llm/openAiCompatibleClient.ts b/packages/server/llm/openAiCompatibleClient.ts new file mode 100644 index 000000000..bb79591d3 --- /dev/null +++ b/packages/server/llm/openAiCompatibleClient.ts @@ -0,0 +1,15 @@ +import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; +import type { LanguageModel } from "ai"; +import type { CustomModelConfig } from "../../protocol/types.js"; + +/** Creates an OpenAI Chat Completions-compatible model for a caller-controlled endpoint. */ +export function createOpenAICompatibleLanguageModel( + config: CustomModelConfig, +): LanguageModel { + return createOpenAICompatible({ + name: "openai-compatible", + apiKey: config.apiKey, + baseURL: config.baseURL, + headers: config.headers, + }).chatModel(config.modelName); +} diff --git a/packages/server/package.json b/packages/server/package.json index 2fded7683..355dfa73e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -13,6 +13,7 @@ "@ai-sdk/google": "catalog:", "@ai-sdk/groq": "catalog:", "@ai-sdk/openai": "catalog:", + "@ai-sdk/openai-compatible": "catalog:", "@ai-sdk/provider": "catalog:", "@opentelemetry/api": "catalog:", "@opentelemetry/core": "catalog:", diff --git a/packages/server/runtime.ts b/packages/server/runtime.ts index ac8a6e0cd..9ac5cb04b 100644 --- a/packages/server/runtime.ts +++ b/packages/server/runtime.ts @@ -97,6 +97,7 @@ import { bytesToBase64 } from "./understudy/fileUploadUtils.js"; import { createStore } from "zustand/vanilla"; import type { StagehandLogEmitter } from "./logger.js"; import { StagehandLogger } from "./logger.js"; +import { buildGatewayContext } from "./llm/gatewayClient.js"; import * as llmService from "./services/llmService.js"; import { StagehandRuntimeStateSchema, type StagehandRuntimeState } from "./runtimeState.js"; import { createStagehandTracing, type StagehandTracing } from "./tracing.js"; @@ -322,7 +323,9 @@ export class StagehandRuntime { throw new Error("An LLM was not configured during Stagehand initialization"); } - return await llmService.generate(model, input, this.adapters.clientLLMGenerate); + const gateway = + state.status === "initialized" ? buildGatewayContext(state.initParams) : undefined; + return await llmService.generate(model, input, this.adapters.clientLLMGenerate, gateway); } async contextPages(): Promise { diff --git a/packages/server/services/actService.ts b/packages/server/services/actService.ts index 9f806a68d..605aee3e2 100644 --- a/packages/server/services/actService.ts +++ b/packages/server/services/actService.ts @@ -16,6 +16,7 @@ import { createTimeoutGuard } from "../handlers/handlerUtils/timeoutGuard.js"; import { resolveVariableValue } from "../handlers/handlerUtils/variables.js"; import * as inference from "../inference.js"; import type { ClientLlmRequest } from "../llm/clientLlmClient.js"; +import type { GatewayContext } from "../llm/gatewayClient.js"; import type { StagehandLogger } from "../logger.js"; import { buildActPrompt, buildStepTwoPrompt } from "../prompt.js"; import type { EncodedId } from "../types/private/internal.js"; @@ -38,6 +39,7 @@ type ActContext = { selfHeal: boolean; domSettleTimeoutMs?: number; ensureTimeRemaining: () => void; + gateway?: GatewayContext; }; export async function act({ @@ -50,6 +52,7 @@ export async function act({ selfHeal = false, domSettleTimeoutMs, cache, + gateway, }: { params: StagehandActParams; page: Page; @@ -60,6 +63,7 @@ export async function act({ selfHeal?: boolean; domSettleTimeoutMs?: number; cache?: cacheService.CacheContext; + gateway?: GatewayContext; }): Promise { const { input, options } = params; const variables = options?.variables; @@ -74,6 +78,7 @@ export async function act({ selfHeal, domSettleTimeoutMs, ensureTimeRemaining, + gateway, }; ensureTimeRemaining(); @@ -234,7 +239,8 @@ async function getActionFromLLM({ const response = await inference.act({ instruction, domElements, - generate: (input) => llmService.generate(context.model, input, context.clientLLMGenerate), + generate: (input) => + llmService.generate(context.model, input, context.clientLLMGenerate, context.gateway), userProvidedInstructions: context.systemPrompt, }); diff --git a/packages/server/services/cacheService.ts b/packages/server/services/cacheService.ts index 20a33d6b7..4ede38014 100644 --- a/packages/server/services/cacheService.ts +++ b/packages/server/services/cacheService.ts @@ -9,12 +9,12 @@ import type { } from "../../protocol/types.js"; import { CacheClient, - apiUrlForRegion, type CacheGetResponse, type CacheLlmUsage, type CacheMethod, type CdpTree, } from "../clients/cacheClient.js"; +import { apiUrlForRegion } from "../clients/stagehandApi.js"; import type { StagehandLogger } from "../logger.js"; import type { Frame } from "../understudy/frame.js"; import { FrameSelectorResolver } from "../understudy/selectorResolver.js"; diff --git a/packages/server/services/extractService.ts b/packages/server/services/extractService.ts index 0880cc8a6..d46b83422 100644 --- a/packages/server/services/extractService.ts +++ b/packages/server/services/extractService.ts @@ -8,6 +8,7 @@ import type { import { TimeoutError } from "../errors.js"; import * as inference from "../inference.js"; import type { ClientLlmRequest } from "../llm/clientLlmClient.js"; +import type { GatewayContext } from "../llm/gatewayClient.js"; import type { StagehandLogger } from "../logger.js"; import type { Page } from "../understudy/page.js"; import type { EncodedId, ZodPathSegments } from "../types/private/internal.js"; @@ -43,6 +44,7 @@ export async function extract({ logger, systemPrompt = "", cache, + gateway, }: { params: StagehandExtractParams; page: Pick; @@ -51,6 +53,7 @@ export async function extract({ logger: StagehandLogger; systemPrompt?: string; cache?: cacheService.CacheContext; + gateway?: GatewayContext; }): Promise { const { instruction, options } = params; const ensureTimeRemaining = createTimeoutGuard( @@ -106,7 +109,7 @@ export async function extract({ instruction, domElements: combinedTree, schema: transformedSchema as z.ZodObject, - generate: (input) => llmService.generate(model, input, clientLLMGenerate), + generate: (input) => llmService.generate(model, input, clientLLMGenerate, gateway), userProvidedInstructions: systemPrompt, }); ensureTimeRemaining(); diff --git a/packages/server/services/llmService.ts b/packages/server/services/llmService.ts index f3bab48a8..ad74d74e5 100644 --- a/packages/server/services/llmService.ts +++ b/packages/server/services/llmService.ts @@ -7,12 +7,15 @@ import type { import { LLMGenerateParamsSchema } from "../../protocol/schemas.js"; import { createAiSdkLanguageModel, generateWithAiSdk } from "../llm/aiSdkClient.js"; import { generateWithClientLlm, type ClientLlmRequest } from "../llm/clientLlmClient.js"; +import { createGatewayLanguageModel, type GatewayContext } from "../llm/gatewayClient.js"; +import { createOpenAICompatibleLanguageModel } from "../llm/openAiCompatibleClient.js"; /** Generates a Stagehand LLM result using the configured local or connected client. */ export async function generate( model: ModelConfig | ClientModelReference, input: LLMGenerateParams, clientRequest: ClientLlmRequest, + gateway?: GatewayContext, ): Promise { const params = LLMGenerateParamsSchema.parse(input); @@ -21,12 +24,16 @@ export async function generate( } if ("baseURL" in model) { - throw new Error("Custom OpenAI-compatible inference is not implemented yet"); + return await generateWithAiSdk(createOpenAICompatibleLanguageModel(model), params); } if (!model.apiKey) { - // TODO: Send configurations without direct credentials through Browserbase Model Gateway. - throw new Error("Direct model inference requires an API key"); + if (!gateway) { + throw new Error( + "Direct model inference requires an API key, or a Browserbase session so requests can run through the model gateway", + ); + } + return await generateWithAiSdk(createGatewayLanguageModel(model, gateway), params); } return await generateWithAiSdk(createAiSdkLanguageModel(model), params); diff --git a/packages/server/services/observeService.ts b/packages/server/services/observeService.ts index 13359118d..07ad39518 100644 --- a/packages/server/services/observeService.ts +++ b/packages/server/services/observeService.ts @@ -9,6 +9,7 @@ import { TimeoutError } from "../errors.js"; import { createTimeoutGuard } from "../handlers/handlerUtils/timeoutGuard.js"; import * as inference from "../inference.js"; import type { ClientLlmRequest } from "../llm/clientLlmClient.js"; +import type { GatewayContext } from "../llm/gatewayClient.js"; import type { StagehandLogger } from "../logger.js"; import type { Page } from "../understudy/page.js"; import { SupportedUnderstudyAction } from "../types/private/handlers.js"; @@ -28,6 +29,7 @@ export async function observe({ logger, systemPrompt = "", cache, + gateway, }: { params: StagehandObserveParams; page: Pick; @@ -36,6 +38,7 @@ export async function observe({ logger: StagehandLogger; systemPrompt?: string; cache?: cacheService.CacheContext; + gateway?: GatewayContext; }): Promise { const { instruction, options } = params; const ensureTimeRemaining = createTimeoutGuard( @@ -83,7 +86,7 @@ export async function observe({ const observation = await inference.observe({ instruction: effectiveInstruction, domElements: combinedTree, - generate: (input) => llmService.generate(model, input, clientLLMGenerate), + generate: (input) => llmService.generate(model, input, clientLLMGenerate, gateway), userProvidedInstructions: systemPrompt, supportedActions: Object.values(SupportedUnderstudyAction), variables: options?.variables, diff --git a/packages/server/tests/ai-sdk-client.test.ts b/packages/server/tests/ai-sdk-client.test.ts index cb62b52d9..90edc7fe7 100644 --- a/packages/server/tests/ai-sdk-client.test.ts +++ b/packages/server/tests/ai-sdk-client.test.ts @@ -20,7 +20,7 @@ describe("AI SDK language models", () => { name: "OpenAI", modelName: "openai/gpt-5.4-mini" as const, modelId: "gpt-5.4-mini", - provider: "openai.responses", + provider: "openai.chat", }, { name: "Anthropic", @@ -85,7 +85,7 @@ describe("AI SDK language models", () => { expect(generateText).toHaveBeenCalledWith( expect.objectContaining({ model: expect.objectContaining({ - provider: "openai.responses", + provider: "openai.chat", modelId: "gpt-5.4-mini", }), }), diff --git a/packages/server/tests/gateway-client.test.ts b/packages/server/tests/gateway-client.test.ts new file mode 100644 index 000000000..7219f450a --- /dev/null +++ b/packages/server/tests/gateway-client.test.ts @@ -0,0 +1,92 @@ +import { generateText } from "ai"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { buildGatewayContext, createGatewayLanguageModel } from "../llm/gatewayClient.js"; +import * as llmService from "../services/llmService.js"; +import type { StagehandInitParams } from "../../protocol/types.js"; + +vi.mock("ai", () => ({ + generateText: vi.fn(), + Output: { + object: vi.fn((options: unknown) => options), + }, +})); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +const initParams = { + model: { modelName: "openai/gpt-5" }, + apiKey: "bb-api-key", + browser: { sessionId: "session-123", region: "eu-central-1" }, +} as StagehandInitParams; + +describe("buildGatewayContext", () => { + it("builds the context from the Browserbase API key, session, and region", () => { + expect(buildGatewayContext(initParams)).toEqual({ + apiUrl: "https://api.euc1.stagehand.browserbase.com/v1", + apiKey: "bb-api-key", + sessionId: "session-123", + }); + }); + + it("returns undefined without a Browserbase API key or session", () => { + expect(buildGatewayContext({ ...initParams, apiKey: undefined })).toBeUndefined(); + expect(buildGatewayContext({ ...initParams, browser: undefined })).toBeUndefined(); + }); +}); + +describe("createGatewayLanguageModel", () => { + it("creates a Responses API model against the gateway endpoint with the full model slug", () => { + const model = createGatewayLanguageModel( + { modelName: "openai/gpt-5" }, + { + apiUrl: "https://api.stagehand.browserbase.com/v1", + apiKey: "bb-api-key", + sessionId: "session-123", + }, + ); + + expect(model).toMatchObject({ + provider: "openai.responses", + modelId: "openai/gpt-5", + }); + }); +}); + +describe("llmService.generate gateway routing", () => { + const input = { + messages: [{ role: "user" as const, content: { type: "text" as const, text: "Hi" } }], + }; + + it("routes key-less model configurations through the Browserbase gateway", async () => { + vi.mocked(generateText).mockResolvedValue({ + text: "Hello", + output: undefined, + finishReason: "stop", + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + } as never); + + await llmService.generate({ modelName: "openai/gpt-5" }, input, vi.fn(), { + apiUrl: "https://api.stagehand.browserbase.com/v1", + apiKey: "bb-api-key", + sessionId: "session-123", + }); + + expect(generateText).toHaveBeenCalledWith( + expect.objectContaining({ + model: expect.objectContaining({ + provider: "openai.responses", + modelId: "openai/gpt-5", + }), + }), + ); + }); + + it("rejects key-less model configurations when no gateway context is available", async () => { + await expect( + llmService.generate({ modelName: "openai/gpt-5" }, input, vi.fn()), + ).rejects.toThrow(/requires an API key, or a Browserbase session/); + expect(generateText).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/server/tests/gateway-e2e.test.ts b/packages/server/tests/gateway-e2e.test.ts new file mode 100644 index 000000000..322b25722 --- /dev/null +++ b/packages/server/tests/gateway-e2e.test.ts @@ -0,0 +1,144 @@ +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import * as llmService from "../services/llmService.js"; +import type { GatewayContext } from "../llm/gatewayClient.js"; + +/** + * End-to-end test of the gateway inference path with nothing mocked: the real + * AI SDK stack (generateText → @ai-sdk/openai responses model → fetch) runs + * against a local HTTP server standing in for stagehand-api-v3's + * POST /v1/llm/responses. Verifies the exact wire contract the server + * expects — endpoint path, auth headers, Responses-format body — and that + * Responses payloads map back into the Stagehand LLM result schema. + */ + +interface CapturedRequest { + url: string; + headers: Record; + body: Record; +} + +let server: Server; +let gateway: GatewayContext; +const requests: CapturedRequest[] = []; +let respondWith: () => Record; + +const completion = (content: string) => ({ + id: "resp_e2e", + object: "response", + created_at: 1700000000, + model: "openai/gpt-5", + status: "completed", + output: [ + { + type: "message", + id: "msg_1", + status: "completed", + role: "assistant", + content: [{ type: "output_text", text: content, annotations: [] }], + }, + ], + usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 }, +}); + +beforeAll(async () => { + server = createServer((req, res) => { + let body = ""; + req.on("data", (chunk: Buffer) => (body += chunk.toString())); + req.on("end", () => { + requests.push({ + url: req.url ?? "", + headers: req.headers, + body: JSON.parse(body) as Record, + }); + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify(respondWith())); + }); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve())); + const { port } = server.address() as AddressInfo; + gateway = { + apiUrl: `http://127.0.0.1:${port}/v1`, + apiKey: "bb-api-key", + sessionId: "session-123", + }; +}); + +afterAll(() => { + server.close(); +}); + +beforeEach(() => { + requests.length = 0; + respondWith = () => completion("Hello from the gateway"); +}); + +describe("gateway inference end to end", () => { + it("sends an OpenAI-format request to the gateway endpoint and maps the response back", async () => { + const result = await llmService.generate( + { modelName: "openai/gpt-5" }, + { + systemPrompt: "Answer concisely.", + messages: [{ role: "user", content: { type: "text", text: "Say hello" } }], + }, + () => { + throw new Error("client LLM must not be used on the gateway path"); + }, + gateway, + ); + + const request = requests[0]!; + expect(request.url).toBe("/v1/llm/responses"); + expect(request.headers["x-bb-api-key"]).toBe("bb-api-key"); + expect(request.headers["x-bb-session-id"]).toBe("session-123"); + expect(request.body.model).toBe("openai/gpt-5"); + expect(request.body.input).toEqual([ + { role: "system", content: "Answer concisely." }, + { + role: "user", + content: [{ type: "input_text", text: "Say hello" }], + }, + ]); + + expect(result).toEqual({ + role: "assistant", + content: { type: "text", text: "Hello from the gateway" }, + stopReason: "stop", + outputFormat: "text", + usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }, + }); + }); + + it("round-trips structured output through the gateway", async () => { + respondWith = () => completion(JSON.stringify({ greeting: "hi" })); + + const result = await llmService.generate( + { modelName: "openai/gpt-5" }, + { + messages: [{ role: "user", content: { type: "text", text: "Greet me" } }], + responseFormat: { + type: "json_schema", + name: "greeting", + schema: { + type: "object", + properties: { greeting: { type: "string" } }, + required: ["greeting"], + additionalProperties: false, + }, + }, + }, + () => { + throw new Error("client LLM must not be used on the gateway path"); + }, + gateway, + ); + + const request = requests[0]!; + const text = request.body.text as { format?: { type: string } } | undefined; + expect(text?.format?.type).toBe("json_schema"); + + expect(result.outputFormat).toBe("json_schema"); + expect(result).toMatchObject({ structuredContent: { greeting: "hi" } }); + }); +}); diff --git a/packages/server/tests/openai-compatible-client.test.ts b/packages/server/tests/openai-compatible-client.test.ts new file mode 100644 index 000000000..3259698cc --- /dev/null +++ b/packages/server/tests/openai-compatible-client.test.ts @@ -0,0 +1,51 @@ +import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createOpenAICompatibleLanguageModel } from "../llm/openAiCompatibleClient.js"; + +const mocks = vi.hoisted(() => ({ + provider: Object.assign(vi.fn(), { chatModel: vi.fn() }), +})); + +vi.mock("@ai-sdk/openai-compatible", () => ({ + createOpenAICompatible: vi.fn(() => mocks.provider), +})); + +describe("OpenAI-compatible language models", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.provider.chatModel.mockReturnValue({ provider: "custom.chat" }); + }); + + it("forwards an authenticated custom endpoint to Chat Completions", () => { + createOpenAICompatibleLanguageModel({ + modelName: "customer-model", + baseURL: "https://customer.example.com/v1", + apiKey: "customer-key", + headers: { "x-tenant-id": "tenant-123" }, + }); + + expect(createOpenAICompatible).toHaveBeenCalledWith({ + name: "openai-compatible", + baseURL: "https://customer.example.com/v1", + apiKey: "customer-key", + headers: { "x-tenant-id": "tenant-123" }, + }); + expect(mocks.provider.chatModel).toHaveBeenCalledWith("customer-model"); + }); + + it("does not invent an API key for a header-only endpoint", () => { + createOpenAICompatibleLanguageModel({ + modelName: "internal-model", + baseURL: "https://models.internal/v1", + headers: { authorization: "Internal secret" }, + }); + + expect(createOpenAICompatible).toHaveBeenCalledWith({ + name: "openai-compatible", + baseURL: "https://models.internal/v1", + apiKey: undefined, + headers: { authorization: "Internal secret" }, + }); + expect(mocks.provider.chatModel).toHaveBeenCalledWith("internal-model"); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 670ae2c10..efd89382f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -220,6 +220,9 @@ catalogs: '@ai-sdk/openai': specifier: ^4.0.8 version: 4.0.8 + '@ai-sdk/openai-compatible': + specifier: ^3.0.5 + version: 3.0.5 '@ai-sdk/provider': specifier: ^4.0.2 version: 4.0.2 @@ -413,7 +416,7 @@ importers: version: 3.1.1 mint: specifier: 'catalog:' - version: 4.2.742(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/node@24.13.2)(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(typescript@5.9.3) + version: 4.2.742(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/node@24.13.2)(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(supports-color@8.1.1)(typescript@5.9.3) packages/evals: dependencies: @@ -462,7 +465,7 @@ importers: version: 24.13.2 braintrust: specifier: ^0.4.10 - version: 0.4.10(zod@4.4.3) + version: 0.4.10(supports-color@8.1.1)(zod@4.4.3) chalk: specifier: ^5.4.1 version: 5.6.2 @@ -563,6 +566,9 @@ importers: '@ai-sdk/openai': specifier: 'catalog:' version: 4.0.8(zod@4.4.3) + '@ai-sdk/openai-compatible': + specifier: 'catalog:' + version: 3.0.5(zod@4.4.3) '@ai-sdk/provider': specifier: 'catalog:' version: 4.0.2 @@ -8538,7 +8544,7 @@ snapshots: dependencies: jsep: 1.4.0 - '@kwsites/file-exists@1.1.1': + '@kwsites/file-exists@1.1.1(supports-color@8.1.1)': dependencies: debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: @@ -8600,10 +8606,10 @@ snapshots: '@types/react': 19.2.17 react: 19.2.3 - '@mintlify/cli@4.0.1345(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/node@24.13.2)(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(typescript@5.9.3)': + '@mintlify/cli@4.0.1345(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/node@24.13.2)(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@inquirer/prompts': 7.9.0(@types/node@24.13.2) - '@mintlify/common': 1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@mintlify/common': 1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3) '@mintlify/link-rot': 3.0.1239(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@mintlify/models': 0.0.343 '@mintlify/prebuild': 1.0.1194(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) @@ -8612,7 +8618,7 @@ snapshots: adm-zip: 0.5.16 chalk: 5.2.0 color: 4.2.3 - detect-port: 1.5.1 + detect-port: 1.5.1(supports-color@8.1.1) front-matter: 4.0.2 fs-extra: 11.2.0 ink: 6.3.0(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.3) @@ -8655,11 +8661,11 @@ snapshots: - typescript - utf-8-validate - '@mintlify/common@1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@mintlify/common@1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@asyncapi/parser': 3.4.0 '@asyncapi/specs': 6.8.1 - '@mintlify/mdx': 3.0.4(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@mintlify/mdx': 3.0.4(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3) '@mintlify/models': 0.0.343 '@mintlify/openapi-parser': 0.0.8 '@mintlify/validation': 0.1.801(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) @@ -8720,7 +8726,7 @@ snapshots: '@mintlify/link-rot@3.0.1239(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': dependencies: - '@mintlify/common': 1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@mintlify/common': 1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3) '@mintlify/models': 0.0.343 '@mintlify/prebuild': 1.0.1194(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@mintlify/previewing': 4.0.1263(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(typescript@5.9.3) @@ -8745,11 +8751,11 @@ snapshots: - typescript - utf-8-validate - '@mintlify/mdx@3.0.4(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@mintlify/mdx@3.0.4(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@radix-ui/react-popover': 1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3) '@shikijs/transformers': 3.23.0 - '@shikijs/twoslash': 3.23.0(typescript@5.9.3) + '@shikijs/twoslash': 3.23.0(supports-color@8.1.1)(typescript@5.9.3) arktype: 2.2.3 hast-util-to-string: 3.0.1 mdast-util-from-markdown: 2.0.3 @@ -8790,7 +8796,7 @@ snapshots: '@mintlify/prebuild@1.0.1194(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': dependencies: - '@mintlify/common': 1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@mintlify/common': 1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3) '@mintlify/openapi-parser': 0.0.8 '@mintlify/scraping': 4.0.911(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@mintlify/validation': 0.1.801(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) @@ -8823,7 +8829,7 @@ snapshots: '@mintlify/previewing@4.0.1263(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(typescript@5.9.3)': dependencies: - '@mintlify/common': 1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@mintlify/common': 1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3) '@mintlify/prebuild': 1.0.1194(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@mintlify/validation': 0.1.801(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) adm-zip: 0.5.16 @@ -8862,7 +8868,7 @@ snapshots: '@mintlify/scraping@4.0.911(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': dependencies: - '@mintlify/common': 1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@mintlify/common': 1.0.1046(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3) '@mintlify/openapi-parser': 0.0.8 fast-xml-parser: 5.7.3 fs-extra: 11.1.1 @@ -8899,7 +8905,7 @@ snapshots: '@mintlify/validation@0.1.801(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': dependencies: - '@mintlify/mdx': 3.0.4(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@mintlify/mdx': 3.0.4(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(supports-color@8.1.1)(typescript@5.9.3) '@mintlify/models': 0.0.343 arktype: 2.1.27 fractional-indexing: 3.2.0 @@ -8931,8 +8937,8 @@ snapshots: cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.1.0 - express: 5.2.1 - express-rate-limit: 8.6.0(express@5.2.1) + express: 5.2.1(supports-color@8.1.1) + express-rate-limit: 8.6.0(express@5.2.1(supports-color@8.1.1)) hono: 4.12.32 jose: 6.2.4 json-schema-typed: 8.0.2 @@ -9549,11 +9555,11 @@ snapshots: '@shikijs/core': 3.23.0 '@shikijs/types': 3.23.0 - '@shikijs/twoslash@3.23.0(typescript@5.9.3)': + '@shikijs/twoslash@3.23.0(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@shikijs/core': 3.23.0 '@shikijs/types': 3.23.0 - twoslash: 0.3.9(typescript@5.9.3) + twoslash: 0.3.9(supports-color@8.1.1)(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -9871,7 +9877,7 @@ snapshots: '@types/node': 25.9.4 optional: true - '@typescript/vfs@1.6.4(typescript@5.9.3)': + '@typescript/vfs@1.6.4(supports-color@8.1.1)(typescript@5.9.3)': dependencies: debug: 4.4.3(supports-color@8.1.1) typescript: 5.9.3 @@ -10252,7 +10258,7 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.3.0: + body-parser@2.3.0(supports-color@8.1.1): dependencies: bytes: 3.1.2 content-type: 2.0.0 @@ -10285,7 +10291,7 @@ snapshots: dependencies: fill-range: 7.1.1 - braintrust@0.4.10(zod@4.4.3): + braintrust@0.4.10(supports-color@8.1.1)(zod@4.4.3): dependencies: '@ai-sdk/provider': 1.1.3 '@next/env': 14.2.35 @@ -10303,7 +10309,7 @@ snapshots: minimatch: 9.0.9 mustache: 4.2.0 pluralize: 8.0.0 - simple-git: 3.36.0 + simple-git: 3.36.0(supports-color@8.1.1) slugify: 1.6.9 source-map: 0.7.6 uuid: 9.0.1 @@ -10732,7 +10738,7 @@ snapshots: detect-node-es@1.1.0: {} - detect-port@1.5.1: + detect-port@1.5.1(supports-color@8.1.1): dependencies: address: 1.2.2 debug: 4.4.3(supports-color@8.1.1) @@ -11118,10 +11124,10 @@ snapshots: expr-eval-fork@3.0.3: {} - express-rate-limit@8.6.0(express@5.2.1): + express-rate-limit@8.6.0(express@5.2.1(supports-color@8.1.1)): dependencies: debug: 4.4.3(supports-color@8.1.1) - express: 5.2.1 + express: 5.2.1(supports-color@8.1.1) ip-address: 10.2.0 transitivePeerDependencies: - supports-color @@ -11162,10 +11168,10 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.2.1: + express@5.2.1(supports-color@8.1.1): dependencies: accepts: 2.0.0 - body-parser: 2.3.0 + body-parser: 2.3.0(supports-color@8.1.1) content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 @@ -11175,7 +11181,7 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1 + finalhandler: 2.1.1(supports-color@8.1.1) fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -11186,8 +11192,8 @@ snapshots: proxy-addr: 2.0.7 qs: 6.15.3 range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 + router: 2.2.0(supports-color@8.1.1) + send: 1.2.1(supports-color@8.1.1) serve-static: 2.2.1 statuses: 2.0.2 type-is: 2.1.0 @@ -11301,7 +11307,7 @@ snapshots: transitivePeerDependencies: - supports-color - finalhandler@2.1.1: + finalhandler@2.1.1(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 @@ -12910,9 +12916,9 @@ snapshots: dependencies: minipass: 7.1.3 - mint@4.2.742(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/node@24.13.2)(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(typescript@5.9.3): + mint@4.2.742(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/node@24.13.2)(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(supports-color@8.1.1)(typescript@5.9.3): dependencies: - '@mintlify/cli': 4.0.1345(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/node@24.13.2)(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(typescript@5.9.3) + '@mintlify/cli': 4.0.1345(@radix-ui/react-popover@1.1.21(@types/react@19.2.17)(react-dom@18.3.1(react@19.2.3))(react@19.2.3))(@types/node@24.13.2)(@types/react@19.2.17)(bufferutil@4.1.0)(react-dom@18.3.1(react@19.2.3))(supports-color@8.1.1)(typescript@5.9.3) transitivePeerDependencies: - '@radix-ui/react-popover' - '@types/node' @@ -13978,7 +13984,7 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.5 '@rolldown/binding-win32-x64-msvc': 1.1.5 - router@2.2.0: + router@2.2.0(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 @@ -14068,7 +14074,7 @@ snapshots: transitivePeerDependencies: - supports-color - send@1.2.1: + send@1.2.1(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 @@ -14103,7 +14109,7 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1 + send: 1.2.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -14257,9 +14263,9 @@ snapshots: simple-concat: 1.0.1 optional: true - simple-git@3.36.0: + simple-git@3.36.0(supports-color@8.1.1): dependencies: - '@kwsites/file-exists': 1.1.1 + '@kwsites/file-exists': 1.1.1(supports-color@8.1.1) '@kwsites/promise-deferred': 1.1.1 '@simple-git/args-pathspec': 1.0.3 '@simple-git/argv-parser': 1.1.1 @@ -14693,9 +14699,9 @@ snapshots: twoslash-protocol@0.3.9: {} - twoslash@0.3.9(typescript@5.9.3): + twoslash@0.3.9(supports-color@8.1.1)(typescript@5.9.3): dependencies: - '@typescript/vfs': 1.6.4(typescript@5.9.3) + '@typescript/vfs': 1.6.4(supports-color@8.1.1)(typescript@5.9.3) twoslash-protocol: 0.3.9 typescript: 5.9.3 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 90e5e925a..c7e192db5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -30,6 +30,7 @@ catalog: ai: ^7.0.16 "@ai-sdk/provider": ^4.0.2 "@ai-sdk/openai": ^4.0.8 + "@ai-sdk/openai-compatible": ^3.0.5 "@ai-sdk/anthropic": ^4.0.8 "@ai-sdk/google": ^4.0.8 "@ai-sdk/groq": ^4.0.5