From efdcadf0d5ed5325cd9eb036fb9bc05a9d561010 Mon Sep 17 00:00:00 2001 From: Lance Tuller Date: Tue, 23 Jun 2026 10:50:49 -0400 Subject: [PATCH] feat(ai): add CoreWeave Serverless Inference provider --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/providers/openai-shared.ts | 18 + packages/ai/src/registry/api-key-login.ts | 2 + .../ai/src/registry/api-key-validation.ts | 8 + packages/ai/src/registry/coreweave.ts | 38 + packages/ai/src/registry/registry.ts | 2 + packages/ai/test/coreweave-login.test.ts | 103 +++ .../ai/test/coreweave-project-header.test.ts | 69 ++ packages/ai/test/provider-registry.test.ts | 7 + packages/catalog/CHANGELOG.md | 1 + packages/catalog/scripts/generate-models.ts | 2 +- packages/catalog/src/index.ts | 1 + packages/catalog/src/models.json | 780 ++++++++++++++++++ .../src/provider-models/descriptors.ts | 8 + .../src/provider-models/openai-compat.ts | 38 +- packages/catalog/src/wire/coreweave.ts | 42 + .../catalog/test/coreweave-provider.test.ts | 124 +++ 17 files changed, 1242 insertions(+), 2 deletions(-) create mode 100644 packages/ai/src/registry/coreweave.ts create mode 100644 packages/ai/test/coreweave-login.test.ts create mode 100644 packages/ai/test/coreweave-project-header.test.ts create mode 100644 packages/catalog/src/wire/coreweave.ts create mode 100644 packages/catalog/test/coreweave-provider.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 5ae02c9c2..bef61f3fb 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -77,6 +77,7 @@ - Fixed OpenRouter Anthropic models on the Responses path omitting `cache_control`, so prompt caching engages without forcing Chat Completions. ([#3397](https://github.com/can1357/oh-my-pi/issues/3397)) - Fixed OpenRouter Anthropic Responses follow-up requests replaying prior reasoning items with stale signatures, which caused HTTP 400 `Invalid signature in thinking block` errors after a thinking turn. ([#3399](https://github.com/can1357/oh-my-pi/issues/3399)) - Fixed OpenRouter Anthropic models on the Responses path omitting `cache_control`, so prompt caching engages without forcing Chat Completions. `cacheRetention: "long"` now upgrades the breakpoint to `ttl: "1h"`. ([#3397](https://github.com/can1357/oh-my-pi/issues/3397)) +- Added CoreWeave Serverless Inference provider login support via `COREWEAVE_API_KEY` and `WANDB_API_KEY` fallback. ## [16.1.16] - 2026-06-23 diff --git a/packages/ai/src/providers/openai-shared.ts b/packages/ai/src/providers/openai-shared.ts index 7d05376f5..fafe8ec70 100644 --- a/packages/ai/src/providers/openai-shared.ts +++ b/packages/ai/src/providers/openai-shared.ts @@ -13,6 +13,11 @@ import type { ResolvedOpenAISharedCompat, VercelGatewayRouting, } from "@oh-my-pi/pi-catalog/types"; +import { + COREWEAVE_PROJECT_HEADER, + coreWeaveProjectHeaders, + hasCoreWeaveProjectHeader, +} from "@oh-my-pi/pi-catalog/wire/coreweave"; import { parseGitHubCopilotApiKey } from "@oh-my-pi/pi-catalog/wire/github-copilot"; import { $env, extractHttpStatusFromError, logger, structuredCloneJSON } from "@oh-my-pi/pi-utils"; import { @@ -141,6 +146,16 @@ function resolveSakanaRequestBaseUrl(): string | undefined { return normalizeSakanaRequestBaseUrl($env.SAKANA_BASE_URL) ?? normalizeSakanaRequestBaseUrl($env.FUGU_BASE_URL); } +function applyCoreWeaveProjectHeader(headers: Record): void { + if (hasCoreWeaveProjectHeader(headers)) { + return; + } + const projectHeaders = coreWeaveProjectHeaders($env); + if (projectHeaders) { + headers[COREWEAVE_PROJECT_HEADER] = projectHeaders[COREWEAVE_PROJECT_HEADER]; + } +} + export function resolveOpenAIRequestSetup( model: OpenAIRequestSetupModel, options: OpenAIRequestSetupOptions, @@ -160,6 +175,9 @@ export function resolveOpenAIRequestSetup( Object.assign(headers, getOpenRouterHeaders()); } Object.assign(headers, options.extraHeaders); + if (model.provider === "coreweave") { + applyCoreWeaveProjectHeader(headers); + } if (options.prependHeaders) { headers = { ...options.prependHeaders(), ...headers }; } diff --git a/packages/ai/src/registry/api-key-login.ts b/packages/ai/src/registry/api-key-login.ts index f11d0e33c..35692ee3e 100644 --- a/packages/ai/src/registry/api-key-login.ts +++ b/packages/ai/src/registry/api-key-login.ts @@ -31,6 +31,7 @@ type ModelsEndpointValidation = { kind: "models-endpoint"; provider: string; modelsUrl: string; + headers?: Record | (() => Record | undefined); }; export type ApiKeyLoginConfig = { @@ -98,6 +99,7 @@ export function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthCon provider: config.validation.provider, apiKey: trimmed, modelsUrl: config.validation.modelsUrl, + headers: config.validation.headers, signal: options.signal, fetch: options.fetch, }); diff --git a/packages/ai/src/registry/api-key-validation.ts b/packages/ai/src/registry/api-key-validation.ts index daac50146..12b2d2352 100644 --- a/packages/ai/src/registry/api-key-validation.ts +++ b/packages/ai/src/registry/api-key-validation.ts @@ -21,6 +21,7 @@ type ModelListValidationOptions = { provider: string; apiKey: string; modelsUrl: string; + headers?: Record | (() => Record | undefined); signal?: AbortSignal; fetch?: FetchImpl; }; @@ -32,6 +33,12 @@ function normalizeAnthropicCompatibleBaseUrl(baseUrl: string): string { return trimmed.endsWith("/v1") ? trimmed.slice(0, -3) : trimmed; } +function resolveValidationHeaders( + headers: Record | (() => Record | undefined) | undefined, +): Record | undefined { + return typeof headers === "function" ? headers() : headers; +} + /** * Validate an API key against an OpenAI-compatible chat completions endpoint. * @@ -129,6 +136,7 @@ export async function validateApiKeyAgainstModelsEndpoint(options: ModelListVali const response = await fetchImpl(options.modelsUrl, { method: "GET", headers: { + ...(resolveValidationHeaders(options.headers) ?? {}), Authorization: `Bearer ${options.apiKey}`, }, signal, diff --git a/packages/ai/src/registry/coreweave.ts b/packages/ai/src/registry/coreweave.ts new file mode 100644 index 000000000..b5c2a56ba --- /dev/null +++ b/packages/ai/src/registry/coreweave.ts @@ -0,0 +1,38 @@ +import { coreWeaveProjectHeaders } from "@oh-my-pi/pi-catalog/wire/coreweave"; +import { $env } from "@oh-my-pi/pi-utils"; +import { createApiKeyLogin } from "./api-key-login"; +import type { OAuthLoginCallbacks } from "./oauth/types"; +import type { ProviderDefinition } from "./types"; + +const PROJECT_SETUP_INSTRUCTIONS = + "Create or select a CoreWeave Serverless Inference project, set COREWEAVE_PROJECT=/ for the OpenAI-Project header, then copy your API key from account settings"; + +function requireCoreWeaveProjectHeaders(): Record { + const headers = coreWeaveProjectHeaders($env); + if (!headers) { + throw new Error( + "CoreWeave Serverless Inference requires OpenAI-Project. Set COREWEAVE_PROJECT=/ before running /login coreweave.", + ); + } + return headers; +} + +export const loginCoreWeave = createApiKeyLogin({ + providerLabel: "CoreWeave Serverless Inference", + authUrl: "https://wandb.ai/settings", + instructions: PROJECT_SETUP_INSTRUCTIONS, + promptMessage: "Paste your CoreWeave Serverless Inference API key", + placeholder: "api-key", + validation: { + kind: "models-endpoint", + provider: "CoreWeave Serverless Inference", + modelsUrl: "https://api.inference.wandb.ai/v1/models", + headers: requireCoreWeaveProjectHeaders, + }, +}); + +export const coreWeaveProvider = { + id: "coreweave", + name: "CoreWeave Serverless Inference", + login: (cb: OAuthLoginCallbacks) => loginCoreWeave(cb), +} as const satisfies ProviderDefinition; diff --git a/packages/ai/src/registry/registry.ts b/packages/ai/src/registry/registry.ts index d089bd89c..cb59260ba 100644 --- a/packages/ai/src/registry/registry.ts +++ b/packages/ai/src/registry/registry.ts @@ -6,6 +6,7 @@ import { anthropicProvider } from "./anthropic"; import { azureProvider } from "./azure"; import { cerebrasProvider } from "./cerebras"; import { cloudflareAiGatewayProvider } from "./cloudflare-ai-gateway"; +import { coreWeaveProvider } from "./coreweave"; import { cursorProvider } from "./cursor"; import { deepseekProvider } from "./deepseek"; import { devinProvider } from "./devin"; @@ -112,6 +113,7 @@ const ALL = [ syntheticProvider, nanogptProvider, waferServerlessProvider, + coreWeaveProvider, vercelAiGatewayProvider, cloudflareAiGatewayProvider, litellmProvider, diff --git a/packages/ai/test/coreweave-login.test.ts b/packages/ai/test/coreweave-login.test.ts new file mode 100644 index 000000000..2516fc4e2 --- /dev/null +++ b/packages/ai/test/coreweave-login.test.ts @@ -0,0 +1,103 @@ +import { afterEach, describe, expect, test, vi } from "bun:test"; +import { loginCoreWeave } from "@oh-my-pi/pi-ai/registry/coreweave"; +import type { FetchImpl } from "@oh-my-pi/pi-ai/types"; + +const COREWEAVE_ENV_KEYS = ["COREWEAVE_PROJECT", "WANDB_INFERENCE_PROJECT", "WANDB_ENTITY", "WANDB_PROJECT"] as const; +const ORIGINAL_ENV = new Map(COREWEAVE_ENV_KEYS.map(key => [key, Bun.env[key]])); + +function restoreCoreWeaveEnv(): void { + for (const key of COREWEAVE_ENV_KEYS) { + const value = ORIGINAL_ENV.get(key); + if (value === undefined) { + delete Bun.env[key]; + } else { + Bun.env[key] = value; + } + } +} + +afterEach(() => { + restoreCoreWeaveEnv(); + vi.restoreAllMocks(); +}); + +describe("CoreWeave Serverless Inference login", () => { + test("validates API key against the models endpoint with the project header", async () => { + Bun.env.COREWEAVE_PROJECT = "team/project"; + delete Bun.env.WANDB_INFERENCE_PROJECT; + delete Bun.env.WANDB_ENTITY; + delete Bun.env.WANDB_PROJECT; + + const fetchMock: FetchImpl = vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + const url = typeof input === "string" ? input : input.toString(); + expect(url).toBe("https://api.inference.wandb.ai/v1/models"); + expect(init?.method).toBe("GET"); + expect(init?.headers).toEqual({ + "OpenAI-Project": "team/project", + Authorization: "Bearer coreweave-test-key", + }); + return new Response(JSON.stringify({ object: "list", data: [] }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }); + + const authMessages: string[] = []; + const apiKey = await loginCoreWeave({ + onAuth: auth => { + if (auth.instructions) { + authMessages.push(auth.instructions); + } + }, + onPrompt: async () => " coreweave-test-key ", + fetch: fetchMock, + }); + + expect(apiKey).toBe("coreweave-test-key"); + expect(authMessages[0]).toContain("COREWEAVE_PROJECT=/"); + expect(authMessages[0]).toContain("OpenAI-Project"); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + test("requires a project header before validating the API key", async () => { + delete Bun.env.COREWEAVE_PROJECT; + delete Bun.env.WANDB_INFERENCE_PROJECT; + delete Bun.env.WANDB_ENTITY; + delete Bun.env.WANDB_PROJECT; + const fetchMock: FetchImpl = vi.fn(async () => { + return new Response(JSON.stringify({ object: "list", data: [] }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }); + + await expect( + loginCoreWeave({ + onPrompt: async () => "coreweave-test-key", + fetch: fetchMock, + }), + ).rejects.toThrow("Set COREWEAVE_PROJECT=/"); + expect(fetchMock).toHaveBeenCalledTimes(0); + }); + + test("surfaces validation errors from the CoreWeave Serverless Inference models endpoint", async () => { + Bun.env.COREWEAVE_PROJECT = "team/project"; + delete Bun.env.WANDB_INFERENCE_PROJECT; + delete Bun.env.WANDB_ENTITY; + delete Bun.env.WANDB_PROJECT; + + const fetchMock: FetchImpl = vi.fn(async () => { + return new Response("Unauthorized", { + status: 401, + headers: { "Content-Type": "text/plain" }, + }); + }); + + await expect( + loginCoreWeave({ + onPrompt: async () => "coreweave-test-key", + fetch: fetchMock, + }), + ).rejects.toThrow("CoreWeave Serverless Inference API key validation failed (401)"); + }); +}); diff --git a/packages/ai/test/coreweave-project-header.test.ts b/packages/ai/test/coreweave-project-header.test.ts new file mode 100644 index 000000000..a01c63ee1 --- /dev/null +++ b/packages/ai/test/coreweave-project-header.test.ts @@ -0,0 +1,69 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { resolveOpenAIRequestSetup } from "@oh-my-pi/pi-ai/providers/openai-shared"; + +const COREWEAVE_ENV_KEYS = ["COREWEAVE_PROJECT", "WANDB_INFERENCE_PROJECT", "WANDB_ENTITY", "WANDB_PROJECT"] as const; +const ORIGINAL_ENV = new Map(COREWEAVE_ENV_KEYS.map(key => [key, Bun.env[key]])); + +function restoreCoreWeaveEnv(): void { + for (const key of COREWEAVE_ENV_KEYS) { + const value = ORIGINAL_ENV.get(key); + if (value === undefined) { + delete Bun.env[key]; + } else { + Bun.env[key] = value; + } + } +} + +afterEach(() => { + restoreCoreWeaveEnv(); +}); + +describe("CoreWeave Serverless Inference project header", () => { + const coreWeaveModel = { + provider: "coreweave", + id: "zai-org/GLM-5.2", + baseUrl: "https://api.inference.wandb.ai/v1", + }; + + test("adds OpenAI-Project from COREWEAVE_PROJECT", () => { + Bun.env.COREWEAVE_PROJECT = "team/project"; + delete Bun.env.WANDB_INFERENCE_PROJECT; + delete Bun.env.WANDB_ENTITY; + delete Bun.env.WANDB_PROJECT; + + const setup = resolveOpenAIRequestSetup(coreWeaveModel, { + apiKey: "coreweave-key", + messages: [], + }); + + expect(setup.headers["OpenAI-Project"]).toBe("team/project"); + }); + + test("builds OpenAI-Project from W&B entity and project fallbacks", () => { + delete Bun.env.COREWEAVE_PROJECT; + delete Bun.env.WANDB_INFERENCE_PROJECT; + Bun.env.WANDB_ENTITY = "wandb-team"; + Bun.env.WANDB_PROJECT = "inference-project"; + + const setup = resolveOpenAIRequestSetup(coreWeaveModel, { + apiKey: "coreweave-key", + messages: [], + }); + + expect(setup.headers["OpenAI-Project"]).toBe("wandb-team/inference-project"); + }); + + test("preserves an explicit request project header", () => { + Bun.env.COREWEAVE_PROJECT = "team/project"; + + const setup = resolveOpenAIRequestSetup(coreWeaveModel, { + apiKey: "coreweave-key", + extraHeaders: { "openai-project": "explicit/team" }, + messages: [], + }); + + expect(setup.headers["openai-project"]).toBe("explicit/team"); + expect(setup.headers["OpenAI-Project"]).toBeUndefined(); + }); +}); diff --git a/packages/ai/test/provider-registry.test.ts b/packages/ai/test/provider-registry.test.ts index f7a2bed73..037264dd1 100644 --- a/packages/ai/test/provider-registry.test.ts +++ b/packages/ai/test/provider-registry.test.ts @@ -14,11 +14,13 @@ import { getEnvApiKey } from "@oh-my-pi/pi-ai/stream"; const FIXTURE_SOURCE = "provider-registry-test"; const ENV_KEYS = [ + "COREWEAVE_API_KEY", "ZENMUX_API_KEY", "EXA_API_KEY", "XAI_OAUTH_TOKEN", "UMANS_AI_CODING_PLAN_API_KEY", "LLAMA_CPP_API_KEY", + "WANDB_API_KEY", ] as const; const originalEnv = new Map(ENV_KEYS.map(key => [key, Bun.env[key]])); @@ -52,6 +54,11 @@ describe("provider registry auth surface", () => { test("multi-var catalog env fallback picks names in order", () => { Bun.env.XAI_OAUTH_TOKEN = "xai-oauth-env"; expect(getEnvApiKey("xai-oauth")).toBe("xai-oauth-env"); + + Bun.env.WANDB_API_KEY = "wandb-env"; + expect(getEnvApiKey("coreweave")).toBe("wandb-env"); + Bun.env.COREWEAVE_API_KEY = "coreweave-env"; + expect(getEnvApiKey("coreweave")).toBe("coreweave-env"); }); test("login list contains loginable providers and excludes env-only model providers", () => { diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index 072669fa5..c379b435d 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -25,6 +25,7 @@ - Fixed the Umans GLM-5.2 thinking-level picker collapsing to a single `high` tier after dynamic discovery: the `max` upstream level now resolves to the internal `xhigh` effort, the picker shows both `high` and `xhigh`, and the metadata maps `xhigh` back to Umans's native `max` wire tier. ([#3192](https://github.com/can1357/oh-my-pi/issues/3192)) - Fixed GitHub Copilot business and enterprise endpoints accepting image inputs that they reject with `400 vision is not supported`. The Copilot `/models` response advertises `capabilities.supports.vision = true` for Claude/GPT chat models on every host, but only the canonical personal endpoint (`https://api.githubcopilot.com`) actually serves them; `githubCopilotModelManagerOptions` now forces `input: ["text"]` whenever discovery resolves to a non-personal base URL, and `mergeDynamicModel` honours the dynamic value (instead of OR-upgrading) when the merged endpoint differs from the bundled reference. ([#3387](https://github.com/can1357/oh-my-pi/issues/3387)) - Fixed OpenRouter Anthropic compat to strip Responses reasoning history during replay so signed thinking blocks are not sent back to routed Anthropic providers. ([#3399](https://github.com/can1357/oh-my-pi/issues/3399)) +- Added CoreWeave Serverless Inference as an OpenAI-compatible provider with models.dev-backed bundled catalog metadata. ## [16.1.14] - 2026-06-22 diff --git a/packages/catalog/scripts/generate-models.ts b/packages/catalog/scripts/generate-models.ts index d8d601d1c..4ed245d8c 100644 --- a/packages/catalog/scripts/generate-models.ts +++ b/packages/catalog/scripts/generate-models.ts @@ -62,7 +62,7 @@ const packageRoot = path.join(import.meta.dir, ".."); * and never written to models.json. */ const DISCOVERY_ONLY_PROVIDERS = new Set(["ollama", "vllm", "lm-studio", "litellm"]); -const RETIRED_PROVIDERS = new Set(["wafer-pass"]); +const RETIRED_PROVIDERS = new Set(["wafer-pass", "wandb"]); async function resolveProviderApiKey(providerId: string, catalog: CatalogDiscoveryConfig): Promise { for (const envVar of catalog.envVars ?? []) { diff --git a/packages/catalog/src/index.ts b/packages/catalog/src/index.ts index a73eea575..dc7c55045 100644 --- a/packages/catalog/src/index.ts +++ b/packages/catalog/src/index.ts @@ -12,5 +12,6 @@ export * from "./types"; export * from "./utils"; export * from "./variant-collapse"; export * from "./wire/codex"; +export * from "./wire/coreweave"; export * from "./wire/gemini-headers"; export * from "./wire/github-copilot"; diff --git a/packages/catalog/src/models.json b/packages/catalog/src/models.json index 892dd9de3..de16152ba 100644 --- a/packages/catalog/src/models.json +++ b/packages/catalog/src/models.json @@ -13487,6 +13487,786 @@ } } }, + "coreweave": { + "deepseek-ai/DeepSeek-V3.1": { + "id": "deepseek-ai/DeepSeek-V3.1", + "name": "DeepSeek V3.1", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.55, + "output": 1.65, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 161000, + "maxTokens": 161000 + }, + "deepseek-ai/DeepSeek-V4-Flash": { + "id": "deepseek-ai/DeepSeek-V4-Flash", + "name": "DeepSeek V4 Flash", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 384000, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "effortMap": { + "minimal": "high", + "low": "high", + "medium": "high", + "high": "high", + "xhigh": "max" + } + } + }, + "deepseek-ai/DeepSeek-V4-Pro": { + "id": "deepseek-ai/DeepSeek-V4-Pro", + "name": "DeepSeek V4 Pro", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 393216, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "effortMap": { + "minimal": "high", + "low": "high", + "medium": "high", + "high": "high", + "xhigh": "max" + } + } + }, + "google/gemma-4-31B-it": { + "id": "google/gemma-4-31B-it", + "name": "Gemma 4 31B Instruct", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 131072, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "ibm-granite/granite-4.1-8b": { + "id": "ibm-granite/granite-4.1-8b", + "name": "Granite 4.1 8B", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 131072 + }, + "JetBrains/Mellum2-12B-A2.5B-Instruct": { + "id": "JetBrains/Mellum2-12B-A2.5B-Instruct", + "name": "JetBrains/Mellum2-12B-A2.5B-Instruct", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": null, + "maxTokens": null + }, + "meta-llama/Llama-3.1-70B-Instruct": { + "id": "meta-llama/Llama-3.1-70B-Instruct", + "name": "Llama 3.1 70B", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.8, + "output": 0.8, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 128000, + "maxTokens": 128000 + }, + "meta-llama/Llama-3.1-8B-Instruct": { + "id": "meta-llama/Llama-3.1-8B-Instruct", + "name": "Meta-Llama-3.1-8B-Instruct", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0.22, + "output": 0.22, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 128000, + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "meta-llama/Llama-3.3-70B-Instruct": { + "id": "meta-llama/Llama-3.3-70B-Instruct", + "name": "Llama-3.3-70B-Instruct", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0.71, + "output": 0.71, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 128000, + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "meta-llama/Llama-4-Scout-17B-16E-Instruct": { + "id": "meta-llama/Llama-4-Scout-17B-16E-Instruct", + "name": "Llama 4 Scout 17B 16E Instruct", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0.17, + "output": 0.66, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 64000, + "maxTokens": 64000, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "microsoft/Phi-4-mini-instruct": { + "id": "microsoft/Phi-4-mini-instruct", + "name": "Phi-4-mini-instruct", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0.08, + "output": 0.35, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 128000, + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "MiniMaxAI/MiniMax-M2.5": { + "id": "MiniMaxAI/MiniMax-M2.5", + "name": "MiniMax M2.5", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.3, + "output": 1.2, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 196608, + "maxTokens": 196608 + }, + "moonshotai/Kimi-K2.5": { + "id": "moonshotai/Kimi-K2.5", + "name": "Kimi K2.5", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0.5, + "output": 2.85, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 262144, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "moonshotai/Kimi-K2.6": { + "id": "moonshotai/Kimi-K2.6", + "name": "Kimi-K2.6", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 262144, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "moonshotai/Kimi-K2.7-Code": { + "id": "moonshotai/Kimi-K2.7-Code", + "name": "Kimi K2.7 Code", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 262144, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8": { + "id": "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8", + "name": "NVIDIA Nemotron 3 Super 120B", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.2, + "output": 0.8, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 262144 + }, + "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B": { + "id": "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B", + "name": "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": null, + "maxTokens": null + }, + "openai/gpt-oss-120b": { + "id": "openai/gpt-oss-120b", + "name": "gpt-oss-120b", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.15, + "output": 0.6, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 131072 + }, + "openai/gpt-oss-20b": { + "id": "openai/gpt-oss-20b", + "name": "gpt-oss-20b", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.05, + "output": 0.2, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": 131072 + }, + "OpenPipe/Qwen3-14B-Instruct": { + "id": "OpenPipe/Qwen3-14B-Instruct", + "name": "OpenPipe Qwen3 14B Instruct", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.05, + "output": 0.22, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 32768, + "maxTokens": 32768 + }, + "Qwen/Qwen3-235B-A22B-Instruct-2507": { + "id": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "name": "Qwen3 235B A22B Instruct 2507", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.1, + "output": 0.1, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 262144 + }, + "Qwen/Qwen3-235B-A22B-Thinking-2507": { + "id": "Qwen/Qwen3-235B-A22B-Thinking-2507", + "name": "Qwen3-235B-A22B-Thinking-2507", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0.1, + "output": 0.1, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 262144, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high" + ], + "requiresEffort": true + } + }, + "Qwen/Qwen3-30B-A3B-Instruct-2507": { + "id": "Qwen/Qwen3-30B-A3B-Instruct-2507", + "name": "Qwen3 30B A3B Instruct 2507", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0.1, + "output": 0.3, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 262144 + }, + "Qwen/Qwen3-Coder-480B-A35B-Instruct": { + "id": "Qwen/Qwen3-Coder-480B-A35B-Instruct", + "name": "Qwen3-Coder-480B-A35B-Instruct", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 1, + "output": 1.5, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 262144 + }, + "Qwen/Qwen3.5-27B": { + "id": "Qwen/Qwen3.5-27B", + "name": "Qwen3.5 27B", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 65536, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high" + ] + } + }, + "Qwen/Qwen3.5-35B-A3B": { + "id": "Qwen/Qwen3.5-35B-A3B", + "name": "Qwen3.5 35B-A3B", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 65536, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high" + ] + } + }, + "Qwen/Qwen3.6-27B": { + "id": "Qwen/Qwen3.6-27B", + "name": "Qwen3.6 27B", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 65536, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high" + ] + } + }, + "Qwen/Qwen3.6-35B-A3B": { + "id": "Qwen/Qwen3.6-35B-A3B", + "name": "Qwen3.6 35B-A3B", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 65536, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high" + ] + } + }, + "zai-org/GLM-5-FP8": { + "id": "zai-org/GLM-5-FP8", + "name": "GLM 5", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 1, + "output": 3.2, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 200000, + "maxTokens": 200000 + }, + "zai-org/GLM-5.1": { + "id": "zai-org/GLM-5.1", + "name": "GLM-5.1", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 1.4, + "output": 4.4, + "cacheRead": 0.26, + "cacheWrite": 0 + }, + "contextWindow": 200000, + "maxTokens": 131072, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + }, + "zai-org/GLM-5.2": { + "id": "zai-org/GLM-5.2", + "name": "GLM-5.2", + "api": "openai-completions", + "provider": "coreweave", + "baseUrl": "https://api.inference.wandb.ai/v1", + "reasoning": true, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 164000, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + } + } + }, "cursor": { "claude-4.5-opus-high": { "id": "claude-4.5-opus-high", diff --git a/packages/catalog/src/provider-models/descriptors.ts b/packages/catalog/src/provider-models/descriptors.ts index 6b9692434..dc2900119 100644 --- a/packages/catalog/src/provider-models/descriptors.ts +++ b/packages/catalog/src/provider-models/descriptors.ts @@ -14,6 +14,7 @@ import { anthropicModelManagerOptions, cerebrasModelManagerOptions, cloudflareAiGatewayModelManagerOptions, + coreWeaveModelManagerOptions, deepseekModelManagerOptions, firepassModelManagerOptions, fireworksModelManagerOptions, @@ -375,6 +376,13 @@ export const CATALOG_PROVIDERS = [ oauthProvider: "wafer-serverless", }, }, + { + id: "coreweave", + defaultModel: "openai/gpt-oss-120b", + envVars: ["COREWEAVE_API_KEY", "WANDB_API_KEY"], + createModelManagerOptions: (config: ModelManagerConfig) => coreWeaveModelManagerOptions(config), + catalogDiscovery: { label: "CoreWeave Serverless Inference" }, + }, { id: "xai", defaultModel: "grok-4-fast-non-reasoning", diff --git a/packages/catalog/src/provider-models/openai-compat.ts b/packages/catalog/src/provider-models/openai-compat.ts index ac1ccff94..0233cb99c 100644 --- a/packages/catalog/src/provider-models/openai-compat.ts +++ b/packages/catalog/src/provider-models/openai-compat.ts @@ -10,6 +10,7 @@ import type { ModelManagerOptions } from "../model-manager"; import { getBundledModels } from "../models"; import type { Api, FetchImpl, Model, ModelSpec, Provider, ThinkingConfig } from "../types"; import { isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils"; +import { coreWeaveProjectHeaders } from "../wire/coreweave"; import { COPILOT_API_HEADERS, getGitHubCopilotBaseUrl, @@ -470,7 +471,19 @@ function isLikelyNanoGptTextModelId(id: string): boolean { return !NANO_GPT_NON_TEXT_MODEL_TOKENS.some(token => normalized.includes(token)); } -type SimpleProviderConfig = { apiKey?: string; baseUrl?: string; fetch?: FetchImpl }; +type SimpleProviderDiscoveryHeaders = Record | (() => Record | undefined); +type SimpleProviderConfig = { + apiKey?: string; + baseUrl?: string; + fetch?: FetchImpl; + headers?: SimpleProviderDiscoveryHeaders; +}; + +function resolveSimpleProviderHeaders( + headers: SimpleProviderDiscoveryHeaders | undefined, +): Record | undefined { + return typeof headers === "function" ? headers() : headers; +} export function createSimpleOpenAICompletionsOptions( providerId: Parameters[0], @@ -489,6 +502,7 @@ export function createSimpleOpenAICompletionsOptions( provider: providerId, baseUrl, apiKey, + headers: resolveSimpleProviderHeaders(config?.headers), mapModel: (entry, defaults) => { const reference = references.get(defaults.id); return mapWithBundledReference(entry, defaults, reference); @@ -516,6 +530,7 @@ function createSimpleOpenAIResponsesOptions( provider: providerId, baseUrl, apiKey, + headers: resolveSimpleProviderHeaders(config?.headers), mapModel: (entry, defaults) => { const reference = references.get(defaults.id); return mapWithBundledReference(entry, defaults, reference); @@ -2480,6 +2495,25 @@ export function togetherModelManagerOptions( return createSimpleOpenAICompletionsOptions("together", "https://api.together.xyz/v1", config); } +// --------------------------------------------------------------------------- +// 15.5 CoreWeave Serverless Inference +// --------------------------------------------------------------------------- + +export interface CoreWeaveModelManagerConfig { + apiKey?: string; + baseUrl?: string; + fetch?: FetchImpl; +} + +export function coreWeaveModelManagerOptions( + config?: CoreWeaveModelManagerConfig, +): ModelManagerOptions<"openai-completions"> { + return createSimpleOpenAICompletionsOptions("coreweave", "https://api.inference.wandb.ai/v1", { + ...config, + headers: () => coreWeaveProjectHeaders(Bun.env), + }); +} + // --------------------------------------------------------------------------- // 16. Moonshot // --------------------------------------------------------------------------- @@ -3653,6 +3687,8 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor openAiCompletionsDescriptor("cerebras", "cerebras", "https://api.cerebras.ai/v1"), // --- Together --- openAiCompletionsDescriptor("togetherai", "together", "https://api.together.xyz/v1"), + // --- CoreWeave Serverless Inference --- + openAiCompletionsDescriptor("wandb", "coreweave", "https://api.inference.wandb.ai/v1"), // --- NVIDIA --- openAiCompletionsDescriptor("nvidia", "nvidia", "https://integrate.api.nvidia.com/v1", { defaultContextWindow: 131072, diff --git a/packages/catalog/src/wire/coreweave.ts b/packages/catalog/src/wire/coreweave.ts new file mode 100644 index 000000000..8dadadb05 --- /dev/null +++ b/packages/catalog/src/wire/coreweave.ts @@ -0,0 +1,42 @@ +export const COREWEAVE_PROJECT_HEADER = "OpenAI-Project" as const; + +export interface CoreWeaveProjectEnv { + [key: string]: string | undefined; + COREWEAVE_PROJECT?: string; + WANDB_INFERENCE_PROJECT?: string; + WANDB_ENTITY?: string; + WANDB_PROJECT?: string; +} + +function cleanEnvValue(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +export function resolveCoreWeaveProject(env: CoreWeaveProjectEnv): string | undefined { + const explicitProject = cleanEnvValue(env.COREWEAVE_PROJECT) ?? cleanEnvValue(env.WANDB_INFERENCE_PROJECT); + if (explicitProject) { + return explicitProject; + } + + const wandbProject = cleanEnvValue(env.WANDB_PROJECT); + if (!wandbProject) { + return undefined; + } + if (wandbProject.includes("/")) { + return wandbProject; + } + + const wandbEntity = cleanEnvValue(env.WANDB_ENTITY); + return wandbEntity ? `${wandbEntity}/${wandbProject}` : undefined; +} + +export function coreWeaveProjectHeaders(env: CoreWeaveProjectEnv): Record | undefined { + const project = resolveCoreWeaveProject(env); + return project ? { [COREWEAVE_PROJECT_HEADER]: project } : undefined; +} + +export function hasCoreWeaveProjectHeader(headers: Record): boolean { + const normalized = COREWEAVE_PROJECT_HEADER.toLowerCase(); + return Object.keys(headers).some(header => header.toLowerCase() === normalized); +} diff --git a/packages/catalog/test/coreweave-provider.test.ts b/packages/catalog/test/coreweave-provider.test.ts new file mode 100644 index 000000000..dfcd06109 --- /dev/null +++ b/packages/catalog/test/coreweave-provider.test.ts @@ -0,0 +1,124 @@ +import { afterEach, describe, expect, test, vi } from "bun:test"; +import { getBundledModels } from "@oh-my-pi/pi-catalog/models"; +import { DEFAULT_MODEL_PER_PROVIDER, PROVIDER_DESCRIPTORS } from "@oh-my-pi/pi-catalog/provider-models/descriptors"; +import { + coreWeaveModelManagerOptions, + MODELS_DEV_PROVIDER_DESCRIPTORS, + mapModelsDevToModels, +} from "@oh-my-pi/pi-catalog/provider-models/openai-compat"; +import type { FetchImpl } from "@oh-my-pi/pi-catalog/types"; + +const COREWEAVE_ENV_KEYS = ["COREWEAVE_PROJECT", "WANDB_INFERENCE_PROJECT", "WANDB_ENTITY", "WANDB_PROJECT"] as const; +const ORIGINAL_ENV = new Map(COREWEAVE_ENV_KEYS.map(key => [key, Bun.env[key]])); + +function restoreCoreWeaveEnv(): void { + for (const key of COREWEAVE_ENV_KEYS) { + const value = ORIGINAL_ENV.get(key); + if (value === undefined) { + delete Bun.env[key]; + } else { + Bun.env[key] = value; + } + } +} + +afterEach(() => { + restoreCoreWeaveEnv(); + vi.restoreAllMocks(); +}); + +describe("CoreWeave Serverless Inference provider support", () => { + test("registers descriptor, default model, environment key, and bundled models", () => { + const descriptor = PROVIDER_DESCRIPTORS.find(item => item.providerId === "coreweave"); + expect(descriptor).toBeDefined(); + expect(descriptor?.defaultModel).toBe("openai/gpt-oss-120b"); + expect(descriptor?.catalogDiscovery?.label).toBe("CoreWeave Serverless Inference"); + expect(descriptor?.catalogDiscovery?.envVars).toEqual(["COREWEAVE_API_KEY", "WANDB_API_KEY"]); + expect(DEFAULT_MODEL_PER_PROVIDER.coreweave).toBe("openai/gpt-oss-120b"); + + const bundled = getBundledModels("coreweave"); + expect(bundled.find(model => model.id === "openai/gpt-oss-120b")).toMatchObject({ + api: "openai-completions", + provider: "coreweave", + baseUrl: "https://api.inference.wandb.ai/v1", + }); + }); + + test("discovers dynamic models with the CoreWeave project header", async () => { + Bun.env.COREWEAVE_PROJECT = "team/project"; + delete Bun.env.WANDB_INFERENCE_PROJECT; + delete Bun.env.WANDB_ENTITY; + delete Bun.env.WANDB_PROJECT; + + const calls: Array<{ url: string; authorization: string | null; project: string | null }> = []; + const fetchMock: FetchImpl = vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + const headers = new Headers(init?.headers); + calls.push({ + url: input.toString(), + authorization: headers.get("authorization"), + project: headers.get("openai-project"), + }); + return new Response( + JSON.stringify({ + data: [{ id: "openai/gpt-oss-120b", name: "GPT OSS 120B" }, { id: "meta-llama/Llama-3.1-8B-Instruct" }], + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + }) as unknown as FetchImpl; + + const options = coreWeaveModelManagerOptions({ apiKey: "coreweave-test-key", fetch: fetchMock }); + const models = await options.fetchDynamicModels?.(); + + expect(options.providerId).toBe("coreweave"); + expect(calls).toEqual([ + { + url: "https://api.inference.wandb.ai/v1/models", + authorization: "Bearer coreweave-test-key", + project: "team/project", + }, + ]); + expect(models?.find(model => model.id === "openai/gpt-oss-120b")).toMatchObject({ + id: "openai/gpt-oss-120b", + name: "GPT OSS 120B", + api: "openai-completions", + provider: "coreweave", + baseUrl: "https://api.inference.wandb.ai/v1", + }); + }); + + test("maps models.dev wandb metadata into OpenAI chat completions models", () => { + const mapped = mapModelsDevToModels( + { + wandb: { + models: { + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT OSS 120B", + tool_call: true, + reasoning: true, + modalities: { input: ["text"] }, + limit: { context: 131072, output: 32768 }, + cost: { input: 0.15, output: 0.6 }, + }, + }, + }, + }, + MODELS_DEV_PROVIDER_DESCRIPTORS, + ); + + expect(mapped.find(model => model.provider === "coreweave")).toMatchObject({ + id: "openai/gpt-oss-120b", + name: "GPT OSS 120B", + api: "openai-completions", + provider: "coreweave", + baseUrl: "https://api.inference.wandb.ai/v1", + reasoning: true, + contextWindow: 131072, + maxTokens: 32768, + cost: { input: 0.15, output: 0.6, cacheRead: 0, cacheWrite: 0 }, + }); + }); +});