feat: integrate Pi backend and Pi Web

This commit is contained in:
luckyyzh
2026-07-30 19:37:53 +08:00
commit 7392ab9dd7
1390 changed files with 337197 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
import { createImagesModels, type ImagesProvider, type MutableImagesModels } from "../images-models.ts";
import { MODELS } from "../models.generated.ts";
import { type CreateModelsOptions, createModels, type MutableModels, type Provider } from "../models.ts";
import type { Api, Model } from "../types.ts";
import { amazonBedrockProvider } from "./amazon-bedrock.ts";
import { antLingProvider } from "./ant-ling.ts";
import { anthropicProvider } from "./anthropic.ts";
import { azureOpenAIResponsesProvider } from "./azure-openai-responses.ts";
import { cerebrasProvider } from "./cerebras.ts";
import { cloudflareAIGatewayProvider } from "./cloudflare-ai-gateway.ts";
import { cloudflareWorkersAIProvider } from "./cloudflare-workers-ai.ts";
import modelDataManifest from "./data/.manifest.json" with { type: "json" };
import { deepseekProvider } from "./deepseek.ts";
import { fireworksProvider } from "./fireworks.ts";
import { githubCopilotProvider } from "./github-copilot.ts";
import { googleProvider } from "./google.ts";
import { googleVertexProvider } from "./google-vertex.ts";
import { groqProvider } from "./groq.ts";
import { huggingfaceProvider } from "./huggingface.ts";
import { kimiCodingProvider } from "./kimi-coding.ts";
import { minimaxProvider } from "./minimax.ts";
import { minimaxCnProvider } from "./minimax-cn.ts";
import { mistralProvider } from "./mistral.ts";
import { moonshotaiProvider } from "./moonshotai.ts";
import { moonshotaiCnProvider } from "./moonshotai-cn.ts";
import { nvidiaProvider } from "./nvidia.ts";
import { openaiProvider } from "./openai.ts";
import { openaiCodexProvider } from "./openai-codex.ts";
import { opencodeProvider } from "./opencode.ts";
import { opencodeGoProvider } from "./opencode-go.ts";
import { openrouterProvider } from "./openrouter.ts";
import { openrouterImagesProvider } from "./openrouter-images.ts";
import { qwenTokenPlanProvider } from "./qwen-token-plan.ts";
import { qwenTokenPlanCnProvider } from "./qwen-token-plan-cn.ts";
import { radiusProvider } from "./radius.ts";
import { togetherProvider } from "./together.ts";
import { vercelAIGatewayProvider } from "./vercel-ai-gateway.ts";
import { xaiProvider } from "./xai.ts";
import { xiaomiProvider } from "./xiaomi.ts";
import { xiaomiTokenPlanAmsProvider } from "./xiaomi-token-plan-ams.ts";
import { xiaomiTokenPlanCnProvider } from "./xiaomi-token-plan-cn.ts";
import { xiaomiTokenPlanSgpProvider } from "./xiaomi-token-plan-sgp.ts";
import { zaiProvider } from "./zai.ts";
import { zaiCodingCnProvider } from "./zai-coding-cn.ts";
export { radiusProvider };
/** Providers present in the generated catalog. `KnownProvider` additionally
* includes purely dynamic providers (e.g. "radius") that have no static
* catalog entry. */
export type BuiltinProvider = keyof typeof MODELS;
type BuiltinModelApi<
TProvider extends BuiltinProvider,
TModelId extends keyof (typeof MODELS)[TProvider],
> = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never;
/** Typed read of the generated built-in catalog. */
export function getBuiltinModel<TProvider extends BuiltinProvider, TModelId extends keyof (typeof MODELS)[TProvider]>(
provider: TProvider,
modelId: TModelId,
): Model<BuiltinModelApi<TProvider, TModelId>> {
const models = MODELS[provider] as Record<string, Model<Api>> | undefined;
return models?.[modelId as string] as Model<BuiltinModelApi<TProvider, TModelId>>;
}
export function getBuiltinProviders(): BuiltinProvider[] {
return Object.keys(MODELS) as BuiltinProvider[];
}
/** Generation timestamp shared by all built-in provider catalogs. */
export function getBuiltinModelDataGeneratedAt(): number | undefined {
const generatedAt = Date.parse(modelDataManifest.generatedAt);
return Number.isNaN(generatedAt) ? undefined : generatedAt;
}
export function getBuiltinModels<TProvider extends BuiltinProvider>(
provider: TProvider,
): Model<BuiltinModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[] {
const models = MODELS[provider] as Record<string, Model<Api>> | undefined;
return models
? (Object.values(models) as Model<BuiltinModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[])
: [];
}
/** All built-in providers, freshly constructed. */
export function builtinProviders(): Provider[] {
return [
amazonBedrockProvider(),
antLingProvider(),
anthropicProvider(),
azureOpenAIResponsesProvider(),
cerebrasProvider(),
cloudflareAIGatewayProvider(),
cloudflareWorkersAIProvider(),
deepseekProvider(),
fireworksProvider(),
githubCopilotProvider(),
googleProvider(),
googleVertexProvider(),
groqProvider(),
huggingfaceProvider(),
kimiCodingProvider(),
minimaxProvider(),
minimaxCnProvider(),
mistralProvider(),
moonshotaiProvider(),
moonshotaiCnProvider(),
nvidiaProvider(),
openaiProvider(),
openaiCodexProvider(),
opencodeProvider(),
opencodeGoProvider(),
openrouterProvider(),
qwenTokenPlanProvider(),
qwenTokenPlanCnProvider(),
radiusProvider(),
togetherProvider(),
vercelAIGatewayProvider(),
xaiProvider(),
xiaomiProvider(),
xiaomiTokenPlanAmsProvider(),
xiaomiTokenPlanCnProvider(),
xiaomiTokenPlanSgpProvider(),
zaiProvider(),
zaiCodingCnProvider(),
];
}
/** A `Models` collection with every built-in provider registered. */
export function builtinModels(options?: CreateModelsOptions): MutableModels {
const models = createModels(options);
for (const provider of builtinProviders()) {
models.setProvider(provider);
}
return models;
}
/** All built-in image-generation providers, freshly constructed. */
export function builtinImagesProviders(): ImagesProvider[] {
return [openrouterImagesProvider()];
}
/** An `ImagesModels` collection with every built-in image-generation provider registered. */
export function builtinImagesModels(options?: CreateModelsOptions): MutableImagesModels {
const models = createImagesModels(options);
for (const provider of builtinImagesProviders()) {
models.setProvider(provider);
}
return models;
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/amazon-bedrock.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const AMAZON_BEDROCK_MODELS: ModelCatalog<typeof values, "amazon-bedrock"> =
flattenModelCatalog("amazon-bedrock", values);
@@ -0,0 +1,82 @@
import { bedrockConverseStreamApi } from "../api/bedrock-converse-stream.lazy.ts";
import type { ApiKeyAuth } from "../auth/types.ts";
import { createProvider, type Provider } from "../models.ts";
import { AMAZON_BEDROCK_MODELS } from "./amazon-bedrock.models.ts";
/**
* Bedrock accepts a bearer token or the AWS SDK's default credential chain.
* The login flow can store a token/profile choice; resolve also detects ambient
* AWS credentials without copying them into pi's credential store.
*/
const bedrockAuth: ApiKeyAuth = {
name: "AWS credentials or bearer token",
login: async (interaction) => {
const method = await interaction.prompt({
type: "select",
message: "Select Amazon Bedrock authentication method:",
options: [
{ id: "bearer-token", label: "Bearer token" },
{ id: "aws-profile", label: "AWS profile" },
{ id: "credential-chain", label: "Existing AWS credential chain" },
],
});
if (method === "bearer-token") {
return {
type: "api_key",
key: await interaction.prompt({ type: "secret", message: "Enter Amazon Bedrock bearer token" }),
};
}
interaction.notify({
type: "info",
message: "Amazon Bedrock supports AWS profiles, IAM credentials, and role-based credentials.",
links: [
{
label: "AWS credential provider chain",
url: "https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html",
},
],
});
if (method === "aws-profile") {
return {
type: "api_key",
env: { AWS_PROFILE: await interaction.prompt({ type: "text", message: "Enter AWS profile name" }) },
};
}
if (method !== "credential-chain") throw new Error(`Unknown Amazon Bedrock auth method: ${method}`);
await interaction.prompt({
type: "text",
message: "Configure AWS credentials, then press Enter to continue",
});
return { type: "api_key" };
},
resolve: async ({ ctx, credential }) => {
if (credential?.key) {
return { auth: { apiKey: credential.key }, env: credential.env, source: "stored credential" };
}
if (await ctx.env("AWS_BEARER_TOKEN_BEDROCK")) return { auth: {}, source: "AWS_BEARER_TOKEN_BEDROCK" };
if (credential?.env?.AWS_PROFILE ?? (await ctx.env("AWS_PROFILE"))) {
return {
auth: {},
env: credential?.env,
source: credential?.env?.AWS_PROFILE ? "stored credential" : "AWS_PROFILE",
};
}
if ((await ctx.env("AWS_ACCESS_KEY_ID")) && (await ctx.env("AWS_SECRET_ACCESS_KEY"))) {
return { auth: {}, source: "AWS access keys" };
}
if (await ctx.env("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")) return { auth: {}, source: "ECS task role" };
if (await ctx.env("AWS_CONTAINER_CREDENTIALS_FULL_URI")) return { auth: {}, source: "ECS task role" };
if (await ctx.env("AWS_WEB_IDENTITY_TOKEN_FILE")) return { auth: {}, source: "web identity token" };
return undefined;
},
};
export function amazonBedrockProvider(): Provider<"bedrock-converse-stream"> {
return createProvider({
id: "amazon-bedrock",
name: "Amazon Bedrock",
auth: { apiKey: bedrockAuth },
models: Object.values(AMAZON_BEDROCK_MODELS),
api: bedrockConverseStreamApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/ant-ling.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const ANT_LING_MODELS: ModelCatalog<typeof values, "ant-ling"> =
flattenModelCatalog("ant-ling", values);
+15
View File
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { ANT_LING_MODELS } from "./ant-ling.models.ts";
export function antLingProvider(): Provider<"openai-completions"> {
return createProvider({
id: "ant-ling",
name: "Ant Ling",
baseUrl: "https://api.ant-ling.com/v1",
auth: { apiKey: envApiKeyAuth("Ant Ling API key", ["ANT_LING_API_KEY"]) },
models: Object.values(ANT_LING_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/anthropic.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const ANTHROPIC_MODELS: ModelCatalog<typeof values, "anthropic"> =
flattenModelCatalog("anthropic", values);
+50
View File
@@ -0,0 +1,50 @@
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
import { lazyOAuth } from "../auth/helpers.ts";
import { loadAnthropicOAuth } from "../auth/oauth/load.ts";
import type { ApiKeyAuth } from "../auth/types.ts";
import { ANTHROPIC_API_KEY_ENV, ANTHROPIC_AUTH_TOKEN_ENV, ANTHROPIC_OAUTH_TOKEN_ENV } from "../env-api-keys.ts";
import { createProvider, type Provider } from "../models.ts";
import { ANTHROPIC_MODELS } from "./anthropic.models.ts";
function anthropicApiKeyAuth(): ApiKeyAuth {
return {
name: "Anthropic API key",
login: async (interaction) => ({
type: "api_key",
key: await interaction.prompt({ type: "secret", message: "Enter Anthropic API key" }),
}),
resolve: async ({ ctx, credential }) => {
if (credential?.key) {
return { auth: { apiKey: credential.key }, env: credential.env, source: "stored credential" };
}
const authToken = await ctx.env(ANTHROPIC_AUTH_TOKEN_ENV);
if (authToken) {
return {
auth: { headers: { Authorization: `Bearer ${authToken}` } },
source: ANTHROPIC_AUTH_TOKEN_ENV,
};
}
for (const envVar of [ANTHROPIC_OAUTH_TOKEN_ENV, ANTHROPIC_API_KEY_ENV]) {
const apiKey = await ctx.env(envVar);
if (apiKey) return { auth: { apiKey }, source: envVar };
}
return undefined;
},
};
}
export function anthropicProvider(): Provider<"anthropic-messages"> {
return createProvider({
id: "anthropic",
name: "Anthropic",
baseUrl: "https://api.anthropic.com",
auth: {
apiKey: anthropicApiKeyAuth(),
oauth: lazyOAuth({ name: "Anthropic (Claude Pro/Max)", load: loadAnthropicOAuth }),
},
models: Object.values(ANTHROPIC_MODELS),
api: anthropicMessagesApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/azure-openai-responses.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const AZURE_OPENAI_RESPONSES_MODELS: ModelCatalog<typeof values, "azure-openai-responses"> =
flattenModelCatalog("azure-openai-responses", values);
@@ -0,0 +1,14 @@
import { azureOpenAIResponsesApi } from "../api/azure-openai-responses.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { AZURE_OPENAI_RESPONSES_MODELS } from "./azure-openai-responses.models.ts";
export function azureOpenAIResponsesProvider(): Provider<"azure-openai-responses"> {
return createProvider({
id: "azure-openai-responses",
name: "Azure OpenAI",
auth: { apiKey: envApiKeyAuth("Azure OpenAI API key", ["AZURE_OPENAI_API_KEY"]) },
models: Object.values(AZURE_OPENAI_RESPONSES_MODELS),
api: azureOpenAIResponsesApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/cerebras.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const CEREBRAS_MODELS: ModelCatalog<typeof values, "cerebras"> =
flattenModelCatalog("cerebras", values);
+15
View File
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { CEREBRAS_MODELS } from "./cerebras.models.ts";
export function cerebrasProvider(): Provider<"openai-completions"> {
return createProvider({
id: "cerebras",
name: "Cerebras",
baseUrl: "https://api.cerebras.ai/v1",
auth: { apiKey: envApiKeyAuth("Cerebras API key", ["CEREBRAS_API_KEY"]) },
models: Object.values(CEREBRAS_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/cloudflare-ai-gateway.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const CLOUDFLARE_AI_GATEWAY_MODELS: ModelCatalog<typeof values, "cloudflare-ai-gateway"> =
flattenModelCatalog("cloudflare-ai-gateway", values);
@@ -0,0 +1,23 @@
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { openAIResponsesApi } from "../api/openai-responses.lazy.ts";
import { createProvider, type Provider } from "../models.ts";
import { CLOUDFLARE_AI_GATEWAY_MODELS } from "./cloudflare-ai-gateway.models.ts";
import { cloudflareAIGatewayAuth } from "./cloudflare-auth.ts";
import { cloudflareStreams } from "./cloudflare-stream.ts";
export function cloudflareAIGatewayProvider(): Provider<
"anthropic-messages" | "openai-completions" | "openai-responses"
> {
return createProvider({
id: "cloudflare-ai-gateway",
name: "Cloudflare AI Gateway",
auth: { apiKey: cloudflareAIGatewayAuth() },
models: Object.values(CLOUDFLARE_AI_GATEWAY_MODELS),
api: {
"anthropic-messages": cloudflareStreams(anthropicMessagesApi()),
"openai-completions": cloudflareStreams(openAICompletionsApi()),
"openai-responses": cloudflareStreams(openAIResponsesApi()),
},
});
}
@@ -0,0 +1,96 @@
import type { ApiKeyAuth, ApiKeyCredential, AuthContext } from "../auth/types.ts";
import type { ProviderEnv } from "../types.ts";
const CLOUDFLARE_API_KEY = "CLOUDFLARE_API_KEY";
const CLOUDFLARE_ACCOUNT_ID = "CLOUDFLARE_ACCOUNT_ID";
const CLOUDFLARE_GATEWAY_ID = "CLOUDFLARE_GATEWAY_ID";
type CloudflareAuthKind = "workers-ai" | "ai-gateway";
async function resolveValue(
name: string,
ctx: AuthContext,
credential: ApiKeyCredential | undefined,
): Promise<string | undefined> {
// Per-field merge: prefer the credential value, fall back to ambient env.
// A credential carrying only the API key must still pick up the account /
// gateway id from the environment.
const fromCredential = credential
? name === CLOUDFLARE_API_KEY
? credential.key
: credential.env?.[name]
: undefined;
return fromCredential ?? (await ctx.env(name));
}
async function resolveCloudflareEnv(
kind: CloudflareAuthKind,
ctx: AuthContext,
credential: ApiKeyCredential | undefined,
): Promise<{ apiKey: string; env: ProviderEnv; source: string } | undefined> {
const apiKey = await resolveValue(CLOUDFLARE_API_KEY, ctx, credential);
const accountId = await resolveValue(CLOUDFLARE_ACCOUNT_ID, ctx, credential);
const gatewayId = kind === "ai-gateway" ? await resolveValue(CLOUDFLARE_GATEWAY_ID, ctx, credential) : undefined;
if (!apiKey || !accountId || (kind === "ai-gateway" && !gatewayId)) return undefined;
return {
apiKey,
env: {
CLOUDFLARE_ACCOUNT_ID: accountId,
...(gatewayId ? { CLOUDFLARE_GATEWAY_ID: gatewayId } : {}),
},
source: credential ? "stored credential" : CLOUDFLARE_API_KEY,
};
}
export function cloudflareWorkersAIAuth(): ApiKeyAuth {
return {
name: "Cloudflare API key",
login: async (interaction) => {
const key = await interaction.prompt({ type: "secret", message: "Enter Cloudflare API key" });
const accountId = await interaction.prompt({ type: "text", message: "Enter Cloudflare account ID" });
return { type: "api_key", key, env: { CLOUDFLARE_ACCOUNT_ID: accountId } };
},
resolve: async ({ ctx, credential }) => {
const resolved = await resolveCloudflareEnv("workers-ai", ctx, credential);
if (!resolved) return undefined;
return {
auth: { apiKey: resolved.apiKey },
env: resolved.env,
source: resolved.source,
};
},
};
}
export function cloudflareAIGatewayAuth(): ApiKeyAuth {
return {
name: "Cloudflare API key",
login: async (interaction) => {
const key = await interaction.prompt({ type: "secret", message: "Enter Cloudflare API key" });
const accountId = await interaction.prompt({ type: "text", message: "Enter Cloudflare account ID" });
const gatewayId = await interaction.prompt({ type: "text", message: "Enter Cloudflare AI Gateway ID" });
return {
type: "api_key",
key,
env: { CLOUDFLARE_ACCOUNT_ID: accountId, CLOUDFLARE_GATEWAY_ID: gatewayId },
};
},
resolve: async ({ ctx, credential }) => {
const resolved = await resolveCloudflareEnv("ai-gateway", ctx, credential);
if (!resolved) return undefined;
return {
auth: {
headers: {
"cf-aig-authorization": `Bearer ${resolved.apiKey}`,
Authorization: null,
"x-api-key": null,
},
},
env: resolved.env,
source: resolved.source,
};
},
};
}
@@ -0,0 +1,28 @@
import type { Api, Model, ProviderEnv, ProviderStreams } from "../types.ts";
const CLOUDFLARE_ACCOUNT_ID = "CLOUDFLARE_ACCOUNT_ID";
const CLOUDFLARE_GATEWAY_ID = "CLOUDFLARE_GATEWAY_ID";
export function resolveCloudflareModel<TApi extends Api>(
model: Model<TApi>,
env: ProviderEnv | undefined,
): Model<TApi> {
if (!env) return model;
const baseUrl = model.baseUrl
.replaceAll(`{${CLOUDFLARE_ACCOUNT_ID}}`, env[CLOUDFLARE_ACCOUNT_ID] ?? `{${CLOUDFLARE_ACCOUNT_ID}}`)
.replaceAll(`{${CLOUDFLARE_GATEWAY_ID}}`, env[CLOUDFLARE_GATEWAY_ID] ?? `{${CLOUDFLARE_GATEWAY_ID}}`);
return baseUrl === model.baseUrl ? model : { ...model, baseUrl };
}
/**
* Wrap an API implementation so Cloudflare account/gateway endpoint
* placeholders materialize from the resolved provider env before dispatch.
*/
export function cloudflareStreams(streams: ProviderStreams): ProviderStreams {
return {
stream: (model, context, options) =>
streams.stream(resolveCloudflareModel(model, options?.env), context, options),
streamSimple: (model, context, options) =>
streams.streamSimple(resolveCloudflareModel(model, options?.env), context, options),
};
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/cloudflare-workers-ai.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const CLOUDFLARE_WORKERS_AI_MODELS: ModelCatalog<typeof values, "cloudflare-workers-ai"> =
flattenModelCatalog("cloudflare-workers-ai", values);
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { createProvider, type Provider } from "../models.ts";
import { cloudflareWorkersAIAuth } from "./cloudflare-auth.ts";
import { cloudflareStreams } from "./cloudflare-stream.ts";
import { CLOUDFLARE_WORKERS_AI_MODELS } from "./cloudflare-workers-ai.models.ts";
export function cloudflareWorkersAIProvider(): Provider<"openai-completions"> {
return createProvider({
id: "cloudflare-workers-ai",
name: "Cloudflare Workers AI",
auth: { apiKey: cloudflareWorkersAIAuth() },
models: Object.values(CLOUDFLARE_WORKERS_AI_MODELS),
api: cloudflareStreams(openAICompletionsApi()),
});
}
+4
View File
@@ -0,0 +1,4 @@
declare module "*.json" {
const value: unknown;
export default value;
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/deepseek.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const DEEPSEEK_MODELS: ModelCatalog<typeof values, "deepseek"> =
flattenModelCatalog("deepseek", values);
+15
View File
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { DEEPSEEK_MODELS } from "./deepseek.models.ts";
export function deepseekProvider(): Provider<"openai-completions"> {
return createProvider({
id: "deepseek",
name: "DeepSeek",
baseUrl: "https://api.deepseek.com",
auth: { apiKey: envApiKeyAuth("DeepSeek API key", ["DEEPSEEK_API_KEY"]) },
models: Object.values(DEEPSEEK_MODELS),
api: openAICompletionsApi(),
});
}
+541
View File
@@ -0,0 +1,541 @@
import { createProvider, type Provider } from "../models.ts";
import type {
AssistantMessage,
AssistantMessageEventStream,
Context,
ImageContent,
Message,
Model,
SimpleStreamOptions,
StreamFunction,
StreamOptions,
TextContent,
ThinkingContent,
ToolCall,
ToolResultMessage,
Usage,
} from "../types.ts";
import { createAssistantMessageEventStream } from "../utils/event-stream.ts";
const DEFAULT_API = "faux";
const DEFAULT_PROVIDER = "faux";
const DEFAULT_MODEL_ID = "faux-1";
const DEFAULT_MODEL_NAME = "Faux Model";
const DEFAULT_BASE_URL = "http://localhost:0";
const DEFAULT_MIN_TOKEN_SIZE = 3;
const DEFAULT_MAX_TOKEN_SIZE = 5;
const DEFAULT_USAGE: Usage = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
export interface FauxModelDefinition {
id: string;
name?: string;
reasoning?: boolean;
input?: ("text" | "image")[];
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
contextWindow?: number;
maxTokens?: number;
}
export type FauxContentBlock = TextContent | ThinkingContent | ToolCall;
export function fauxText(text: string): TextContent {
return { type: "text", text };
}
export function fauxThinking(thinking: string): ThinkingContent {
return { type: "thinking", thinking };
}
export function fauxToolCall(name: string, arguments_: ToolCall["arguments"], options: { id?: string } = {}): ToolCall {
return {
type: "toolCall",
id: options.id ?? randomId("tool"),
name,
arguments: arguments_,
};
}
function normalizeFauxAssistantContent(content: string | FauxContentBlock | FauxContentBlock[]): FauxContentBlock[] {
if (typeof content === "string") {
return [fauxText(content)];
}
return Array.isArray(content) ? content : [content];
}
export function fauxAssistantMessage(
content: string | FauxContentBlock | FauxContentBlock[],
options: {
stopReason?: AssistantMessage["stopReason"];
errorMessage?: string;
responseId?: string;
timestamp?: number;
} = {},
): AssistantMessage {
return {
role: "assistant",
content: normalizeFauxAssistantContent(content),
api: DEFAULT_API,
provider: DEFAULT_PROVIDER,
model: DEFAULT_MODEL_ID,
usage: DEFAULT_USAGE,
stopReason: options.stopReason ?? "stop",
errorMessage: options.errorMessage,
responseId: options.responseId,
timestamp: options.timestamp ?? Date.now(),
};
}
export type FauxResponseFactory = (
context: Context,
options: StreamOptions | undefined,
state: { callCount: number },
model: Model<string>,
) => AssistantMessage | Promise<AssistantMessage>;
export type FauxResponseStep = AssistantMessage | FauxResponseFactory;
export interface RegisterFauxProviderOptions {
api?: string;
provider?: string;
models?: FauxModelDefinition[];
tokensPerSecond?: number;
tokenSize?: {
min?: number;
max?: number;
};
}
export interface FauxProviderRegistration {
api: string;
models: [Model<string>, ...Model<string>[]];
getModel(): Model<string>;
getModel(modelId: string): Model<string> | undefined;
state: { callCount: number };
setResponses: (responses: FauxResponseStep[]) => void;
appendResponses: (responses: FauxResponseStep[]) => void;
getPendingResponseCount: () => number;
unregister: () => void;
}
export interface FauxProviderHandle {
provider: Provider;
api: string;
models: [Model<string>, ...Model<string>[]];
getModel(): Model<string>;
getModel(modelId: string): Model<string> | undefined;
state: { callCount: number };
setResponses: (responses: FauxResponseStep[]) => void;
appendResponses: (responses: FauxResponseStep[]) => void;
getPendingResponseCount: () => number;
}
function estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
function randomId(prefix: string): string {
return `${prefix}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
}
function contentToText(content: string | Array<TextContent | ImageContent>): string {
if (typeof content === "string") {
return content;
}
return content
.map((block) => {
if (block.type === "text") {
return block.text;
}
return `[image:${block.mimeType}:${block.data.length}]`;
})
.join("\n");
}
function assistantContentToText(content: Array<TextContent | ThinkingContent | ToolCall>): string {
return content
.map((block) => {
if (block.type === "text") {
return block.text;
}
if (block.type === "thinking") {
return block.thinking;
}
return `${block.name}:${JSON.stringify(block.arguments)}`;
})
.join("\n");
}
function toolResultToText(message: ToolResultMessage): string {
return [message.toolName, ...message.content.map((block) => contentToText([block]))].join("\n");
}
function messageToText(message: Message): string {
if (message.role === "user") {
return contentToText(message.content);
}
if (message.role === "assistant") {
return assistantContentToText(message.content);
}
return toolResultToText(message);
}
function serializeContext(context: Context): string {
const parts: string[] = [];
if (context.systemPrompt) {
parts.push(`system:${context.systemPrompt}`);
}
for (const message of context.messages) {
parts.push(`${message.role}:${messageToText(message)}`);
}
if (context.tools?.length) {
parts.push(`tools:${JSON.stringify(context.tools)}`);
}
return parts.join("\n\n");
}
function commonPrefixLength(a: string, b: string): number {
const length = Math.min(a.length, b.length);
let index = 0;
while (index < length && a[index] === b[index]) {
index++;
}
return index;
}
function withUsageEstimate(
message: AssistantMessage,
context: Context,
options: StreamOptions | undefined,
promptCache: Map<string, string>,
): AssistantMessage {
const promptText = serializeContext(context);
const promptTokens = estimateTokens(promptText);
const outputTokens = estimateTokens(assistantContentToText(message.content));
let input = promptTokens;
let cacheRead = 0;
let cacheWrite = 0;
const sessionId = options?.sessionId;
if (sessionId && options?.cacheRetention !== "none") {
const previousPrompt = promptCache.get(sessionId);
if (previousPrompt) {
const cachedChars = commonPrefixLength(previousPrompt, promptText);
cacheRead = estimateTokens(previousPrompt.slice(0, cachedChars));
cacheWrite = estimateTokens(promptText.slice(cachedChars));
input = Math.max(0, promptTokens - cacheRead);
} else {
cacheWrite = promptTokens;
}
promptCache.set(sessionId, promptText);
}
return {
...message,
usage: {
input,
output: outputTokens,
cacheRead,
cacheWrite,
totalTokens: input + outputTokens + cacheRead + cacheWrite,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
};
}
function splitStringByTokenSize(text: string, minTokenSize: number, maxTokenSize: number): string[] {
const chunks: string[] = [];
let index = 0;
while (index < text.length) {
const tokenSize = minTokenSize + Math.floor(Math.random() * (maxTokenSize - minTokenSize + 1));
const charSize = Math.max(1, tokenSize * 4);
chunks.push(text.slice(index, index + charSize));
index += charSize;
}
return chunks.length > 0 ? chunks : [""];
}
function cloneMessage(message: AssistantMessage, api: string, provider: string, modelId: string): AssistantMessage {
const cloned = structuredClone(message);
return {
...cloned,
api,
provider,
model: modelId,
timestamp: cloned.timestamp ?? Date.now(),
usage: cloned.usage ?? DEFAULT_USAGE,
};
}
function createErrorMessage(error: unknown, api: string, provider: string, modelId: string): AssistantMessage {
return {
role: "assistant",
content: [],
api,
provider,
model: modelId,
usage: DEFAULT_USAGE,
stopReason: "error",
errorMessage: error instanceof Error ? error.message : String(error),
timestamp: Date.now(),
};
}
function createAbortedMessage(partial: AssistantMessage): AssistantMessage {
return {
...partial,
stopReason: "aborted",
errorMessage: "Request was aborted",
timestamp: Date.now(),
};
}
function scheduleChunk(chunk: string, tokensPerSecond: number | undefined): Promise<void> {
if (!tokensPerSecond || tokensPerSecond <= 0) {
return new Promise((resolve) => queueMicrotask(resolve));
}
const delayMs = (estimateTokens(chunk) / tokensPerSecond) * 1000;
return new Promise((resolve) => setTimeout(resolve, delayMs));
}
async function streamWithDeltas(
stream: AssistantMessageEventStream,
message: AssistantMessage,
minTokenSize: number,
maxTokenSize: number,
tokensPerSecond: number | undefined,
signal: AbortSignal | undefined,
): Promise<void> {
const partial: AssistantMessage = { ...message, content: [], stopReason: "pending" };
if (signal?.aborted) {
const aborted = createAbortedMessage(partial);
stream.push({ type: "error", reason: "aborted", error: aborted });
stream.end(aborted);
return;
}
stream.push({ type: "start", partial: { ...partial } });
for (let index = 0; index < message.content.length; index++) {
if (signal?.aborted) {
const aborted = createAbortedMessage(partial);
stream.push({ type: "error", reason: "aborted", error: aborted });
stream.end(aborted);
return;
}
const block = message.content[index];
if (block.type === "thinking") {
partial.content = [...partial.content, { type: "thinking", thinking: "" }];
stream.push({ type: "thinking_start", contentIndex: index, partial: { ...partial } });
for (const chunk of splitStringByTokenSize(block.thinking, minTokenSize, maxTokenSize)) {
await scheduleChunk(chunk, tokensPerSecond);
if (signal?.aborted) {
const aborted = createAbortedMessage(partial);
stream.push({ type: "error", reason: "aborted", error: aborted });
stream.end(aborted);
return;
}
(partial.content[index] as ThinkingContent).thinking += chunk;
stream.push({ type: "thinking_delta", contentIndex: index, delta: chunk, partial: { ...partial } });
}
stream.push({
type: "thinking_end",
contentIndex: index,
content: block.thinking,
partial: { ...partial },
});
continue;
}
if (block.type === "text") {
partial.content = [...partial.content, { type: "text", text: "" }];
stream.push({ type: "text_start", contentIndex: index, partial: { ...partial } });
for (const chunk of splitStringByTokenSize(block.text, minTokenSize, maxTokenSize)) {
await scheduleChunk(chunk, tokensPerSecond);
if (signal?.aborted) {
const aborted = createAbortedMessage(partial);
stream.push({ type: "error", reason: "aborted", error: aborted });
stream.end(aborted);
return;
}
(partial.content[index] as TextContent).text += chunk;
stream.push({ type: "text_delta", contentIndex: index, delta: chunk, partial: { ...partial } });
}
stream.push({ type: "text_end", contentIndex: index, content: block.text, partial: { ...partial } });
continue;
}
partial.content = [...partial.content, { type: "toolCall", id: block.id, name: block.name, arguments: {} }];
stream.push({ type: "toolcall_start", contentIndex: index, partial: { ...partial } });
for (const chunk of splitStringByTokenSize(JSON.stringify(block.arguments), minTokenSize, maxTokenSize)) {
await scheduleChunk(chunk, tokensPerSecond);
if (signal?.aborted) {
const aborted = createAbortedMessage(partial);
stream.push({ type: "error", reason: "aborted", error: aborted });
stream.end(aborted);
return;
}
stream.push({ type: "toolcall_delta", contentIndex: index, delta: chunk, partial: { ...partial } });
}
(partial.content[index] as ToolCall).arguments = block.arguments;
stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: { ...partial } });
}
if (message.stopReason === "pending") {
throw new Error("Faux response ended without a stop reason");
}
if (message.stopReason === "error" || message.stopReason === "aborted") {
stream.push({ type: "error", reason: message.stopReason, error: message });
stream.end(message);
return;
}
stream.push({ type: "done", reason: message.stopReason, message });
stream.end(message);
}
export function createFauxCore(options: RegisterFauxProviderOptions) {
const api = options.api ?? randomId(DEFAULT_API);
const provider = options.provider ?? DEFAULT_PROVIDER;
const minTokenSize = Math.max(
1,
Math.min(options.tokenSize?.min ?? DEFAULT_MIN_TOKEN_SIZE, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE),
);
const maxTokenSize = Math.max(minTokenSize, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE);
let pendingResponses: FauxResponseStep[] = [];
const tokensPerSecond = options.tokensPerSecond;
const state = { callCount: 0 };
const promptCache = new Map<string, string>();
const modelDefinitions = options.models?.length
? options.models
: [
{
id: DEFAULT_MODEL_ID,
name: DEFAULT_MODEL_NAME,
reasoning: false,
input: ["text", "image"] as ("text" | "image")[],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384,
},
];
const models = modelDefinitions.map((definition) => ({
id: definition.id,
name: definition.name ?? definition.id,
api,
provider,
baseUrl: DEFAULT_BASE_URL,
reasoning: definition.reasoning ?? false,
input: definition.input ?? ["text", "image"],
cost: definition.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: definition.contextWindow ?? 128000,
maxTokens: definition.maxTokens ?? 16384,
})) as [Model<string>, ...Model<string>[]];
const stream: StreamFunction<string, StreamOptions> = (requestModel, context, streamOptions) => {
const outer = createAssistantMessageEventStream();
const step = pendingResponses.shift();
state.callCount++;
queueMicrotask(async () => {
try {
await streamOptions?.onResponse?.({ status: 200, headers: {} }, requestModel);
if (!step) {
let message = createErrorMessage(
new Error("No more faux responses queued"),
api,
provider,
requestModel.id,
);
message = withUsageEstimate(message, context, streamOptions, promptCache);
outer.push({ type: "error", reason: "error", error: message });
outer.end(message);
return;
}
const resolved =
typeof step === "function" ? await step(context, streamOptions, state, requestModel) : step;
let message = cloneMessage(resolved, api, provider, requestModel.id);
message = withUsageEstimate(message, context, streamOptions, promptCache);
await streamWithDeltas(outer, message, minTokenSize, maxTokenSize, tokensPerSecond, streamOptions?.signal);
} catch (error) {
const message = createErrorMessage(error, api, provider, requestModel.id);
outer.push({ type: "error", reason: "error", error: message });
outer.end(message);
}
});
return outer;
};
const streamSimple: StreamFunction<string, SimpleStreamOptions> = (streamModel, context, streamOptions) =>
stream(streamModel, context, streamOptions);
function getModel(): Model<string>;
function getModel(requestedModelId: string): Model<string> | undefined;
function getModel(requestedModelId?: string): Model<string> | undefined {
if (!requestedModelId) {
return models[0];
}
return models.find((candidate) => candidate.id === requestedModelId);
}
return {
api,
provider,
models,
stream,
streamSimple,
getModel,
state,
setResponses(responses: FauxResponseStep[]) {
pendingResponses = [...responses];
},
appendResponses(responses: FauxResponseStep[]) {
pendingResponses.push(...responses);
},
getPendingResponseCount() {
return pendingResponses.length;
},
};
}
/**
* Faux provider for tests built on explicit `Models` collections:
*
* ```ts
* const faux = fauxProvider();
* const models = createModels();
* models.setProvider(faux.provider);
* faux.setResponses([fauxAssistantMessage("hi")]);
* ```
*/
export function fauxProvider(options: RegisterFauxProviderOptions = {}): FauxProviderHandle {
const core = createFauxCore(options);
const provider = createProvider({
id: core.provider,
auth: { apiKey: { name: "Faux", resolve: async () => ({ auth: {} }) } },
models: core.models,
api: { stream: core.stream, streamSimple: core.streamSimple },
});
return {
provider,
api: core.api,
models: core.models,
getModel: core.getModel,
state: core.state,
setResponses: core.setResponses,
appendResponses: core.appendResponses,
getPendingResponseCount: core.getPendingResponseCount,
};
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/fireworks.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const FIREWORKS_MODELS: ModelCatalog<typeof values, "fireworks"> =
flattenModelCatalog("fireworks", values);
+19
View File
@@ -0,0 +1,19 @@
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { FIREWORKS_MODELS } from "./fireworks.models.ts";
export function fireworksProvider(): Provider<"anthropic-messages" | "openai-completions"> {
return createProvider({
id: "fireworks",
name: "Fireworks",
baseUrl: "https://api.fireworks.ai/inference",
auth: { apiKey: envApiKeyAuth("Fireworks API key", ["FIREWORKS_API_KEY"]) },
models: Object.values(FIREWORKS_MODELS),
api: {
"anthropic-messages": anthropicMessagesApi(),
"openai-completions": openAICompletionsApi(),
},
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/github-copilot.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const GITHUB_COPILOT_MODELS: ModelCatalog<typeof values, "github-copilot"> =
flattenModelCatalog("github-copilot", values);
@@ -0,0 +1,34 @@
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { openAIResponsesApi } from "../api/openai-responses.lazy.ts";
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
import { loadGitHubCopilotOAuth } from "../auth/oauth/load.ts";
import { createProvider, type Provider } from "../models.ts";
import { GITHUB_COPILOT_MODELS } from "./github-copilot.models.ts";
export function githubCopilotProvider(): Provider<"anthropic-messages" | "openai-completions" | "openai-responses"> {
return createProvider({
id: "github-copilot",
name: "GitHub Copilot",
baseUrl: "https://api.individual.githubcopilot.com",
auth: {
apiKey: envApiKeyAuth("GitHub Copilot token", ["COPILOT_GITHUB_TOKEN"]),
oauth: lazyOAuth({ name: "GitHub Copilot", load: loadGitHubCopilotOAuth }),
},
models: Object.values(GITHUB_COPILOT_MODELS),
filterModels: (models, credential) => {
if (credential?.type !== "oauth") return models;
const availableModelIds = credential.availableModelIds;
if (!Array.isArray(availableModelIds) || !availableModelIds.every((id) => typeof id === "string")) {
return models;
}
const available = new Set(availableModelIds);
return models.filter((model) => available.has(model.id));
},
api: {
"anthropic-messages": anthropicMessagesApi(),
"openai-completions": openAICompletionsApi(),
"openai-responses": openAIResponsesApi(),
},
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/google-vertex.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const GOOGLE_VERTEX_MODELS: ModelCatalog<typeof values, "google-vertex"> =
flattenModelCatalog("google-vertex", values);
@@ -0,0 +1,93 @@
import { googleVertexApi } from "../api/google-vertex.lazy.ts";
import type { ApiKeyAuth } from "../auth/types.ts";
import { createProvider, type Provider } from "../models.ts";
import { GOOGLE_VERTEX_MODELS } from "./google-vertex.models.ts";
const VERTEX_ADC_PATH = "~/.config/gcloud/application_default_credentials.json";
/**
* Vertex accepts an explicit API key or Application Default Credentials
* (`gcloud auth application-default login`). ADC additionally requires
* project and location env vars, which the implementation reads itself.
*/
const vertexAuth: ApiKeyAuth = {
name: "Google Cloud credentials",
login: async (interaction) => {
const method = await interaction.prompt({
type: "select",
message: "Select Google Vertex AI authentication method:",
options: [
{ id: "api-key", label: "Google Cloud API key" },
{ id: "adc", label: "Application Default Credentials" },
{ id: "service-account", label: "Service account credentials file" },
],
});
if (method === "api-key") {
return {
type: "api_key",
key: await interaction.prompt({ type: "secret", message: "Enter Google Cloud API key" }),
};
}
if (method !== "adc" && method !== "service-account") {
throw new Error(`Unknown Google Vertex AI auth method: ${method}`);
}
interaction.notify({
type: "info",
message:
method === "adc"
? "Run `gcloud auth application-default login`, then provide the project and location."
: "Provide a service account credentials file, project, and location.",
links: [
{
label: "Application Default Credentials",
url: "https://cloud.google.com/docs/authentication/provide-credentials-adc",
},
],
});
const project = await interaction.prompt({ type: "text", message: "Enter Google Cloud project ID" });
const location = await interaction.prompt({ type: "text", message: "Enter Google Cloud location" });
const credentialsPath =
method === "service-account"
? await interaction.prompt({ type: "text", message: "Enter service account credentials file path" })
: undefined;
return {
type: "api_key",
env: {
GOOGLE_CLOUD_PROJECT: project,
GOOGLE_CLOUD_LOCATION: location,
...(credentialsPath ? { GOOGLE_APPLICATION_CREDENTIALS: credentialsPath } : {}),
},
};
},
resolve: async ({ ctx, credential }) => {
const key = credential?.key ?? (await ctx.env("GOOGLE_CLOUD_API_KEY"));
if (key) return { auth: { apiKey: key }, source: credential?.key ? "stored credential" : "GOOGLE_CLOUD_API_KEY" };
const adcPath =
credential?.env?.GOOGLE_APPLICATION_CREDENTIALS ?? (await ctx.env("GOOGLE_APPLICATION_CREDENTIALS"));
const hasCredentials = await ctx.fileExists(adcPath ?? VERTEX_ADC_PATH);
const project =
credential?.env?.GOOGLE_CLOUD_PROJECT ??
(await ctx.env("GOOGLE_CLOUD_PROJECT")) ??
(await ctx.env("GCLOUD_PROJECT"));
const location = credential?.env?.GOOGLE_CLOUD_LOCATION ?? (await ctx.env("GOOGLE_CLOUD_LOCATION"));
if (hasCredentials && project && location) {
return {
auth: {},
env: credential?.env,
source: credential ? "stored credential" : "gcloud application default credentials",
};
}
return undefined;
},
};
export function googleVertexProvider(): Provider<"google-vertex"> {
return createProvider({
id: "google-vertex",
name: "Google Vertex AI",
auth: { apiKey: vertexAuth },
models: Object.values(GOOGLE_VERTEX_MODELS),
api: googleVertexApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/google.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const GOOGLE_MODELS: ModelCatalog<typeof values, "google"> =
flattenModelCatalog("google", values);
+15
View File
@@ -0,0 +1,15 @@
import { googleGenerativeAIApi } from "../api/google-generative-ai.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { GOOGLE_MODELS } from "./google.models.ts";
export function googleProvider(): Provider<"google-generative-ai"> {
return createProvider({
id: "google",
name: "Google",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
auth: { apiKey: envApiKeyAuth("Gemini API key", ["GEMINI_API_KEY"]) },
models: Object.values(GOOGLE_MODELS),
api: googleGenerativeAIApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/groq.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const GROQ_MODELS: ModelCatalog<typeof values, "groq"> =
flattenModelCatalog("groq", values);
+15
View File
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { GROQ_MODELS } from "./groq.models.ts";
export function groqProvider(): Provider<"openai-completions"> {
return createProvider({
id: "groq",
name: "Groq",
baseUrl: "https://api.groq.com/openai/v1",
auth: { apiKey: envApiKeyAuth("Groq API key", ["GROQ_API_KEY"]) },
models: Object.values(GROQ_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/huggingface.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const HUGGINGFACE_MODELS: ModelCatalog<typeof values, "huggingface"> =
flattenModelCatalog("huggingface", values);
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { HUGGINGFACE_MODELS } from "./huggingface.models.ts";
export function huggingfaceProvider(): Provider<"openai-completions"> {
return createProvider({
id: "huggingface",
name: "Hugging Face",
baseUrl: "https://router.huggingface.co/v1",
auth: { apiKey: envApiKeyAuth("Hugging Face token", ["HF_TOKEN"]) },
models: Object.values(HUGGINGFACE_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,50 @@
import type { generateImages as generateImagesOpenRouterFunction } from "../../api/openrouter-images.ts";
import { registerImagesApiProvider } from "../../images-api-registry.ts";
import type { AssistantImages, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "../../types.ts";
interface OpenRouterImagesProviderModule {
generateImages: typeof generateImagesOpenRouterFunction;
}
let openRouterImagesProviderModulePromise: Promise<OpenRouterImagesProviderModule> | undefined;
function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, error: unknown): AssistantImages {
return {
api: model.api,
provider: model.provider,
model: model.id,
output: [],
stopReason: "error",
errorMessage: error instanceof Error ? error.message : String(error),
timestamp: Date.now(),
};
}
function loadOpenRouterImagesProviderModule(): Promise<OpenRouterImagesProviderModule> {
openRouterImagesProviderModulePromise ||= import("../../api/openrouter-images.ts").then(
(module) => module as OpenRouterImagesProviderModule,
);
return openRouterImagesProviderModulePromise;
}
export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", ImagesOptions> = async (
model: ImagesModel<"openrouter-images">,
context: ImagesContext,
options?: ImagesOptions,
) => {
try {
const module = await loadOpenRouterImagesProviderModule();
return await module.generateImages(model, context, options);
} catch (error) {
return createLazyLoadErrorImages(model, error);
}
};
export function registerBuiltInImagesApiProviders(): void {
registerImagesApiProvider({
api: "openrouter-images",
generateImages: generateImagesOpenRouter,
});
}
registerBuiltInImagesApiProviders();
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/kimi-coding.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const KIMI_CODING_MODELS: ModelCatalog<typeof values, "kimi-coding"> =
flattenModelCatalog("kimi-coding", values);
@@ -0,0 +1,23 @@
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
import { loadKimiCodingOAuth } from "../auth/oauth/load.ts";
import { createProvider, type Provider } from "../models.ts";
import { KIMI_CODING_MODELS } from "./kimi-coding.models.ts";
export function kimiCodingProvider(): Provider<"anthropic-messages"> {
return createProvider({
id: "kimi-coding",
name: "Kimi For Coding",
baseUrl: "https://api.kimi.com/coding",
auth: {
apiKey: envApiKeyAuth("Kimi API key", ["KIMI_API_KEY"]),
oauth: lazyOAuth({
name: "Kimi Code (subscription)",
loginLabel: "Sign in with Kimi Code",
load: loadKimiCodingOAuth,
}),
},
models: Object.values(KIMI_CODING_MODELS),
api: anthropicMessagesApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/minimax-cn.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const MINIMAX_CN_MODELS: ModelCatalog<typeof values, "minimax-cn"> =
flattenModelCatalog("minimax-cn", values);
@@ -0,0 +1,15 @@
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { MINIMAX_CN_MODELS } from "./minimax-cn.models.ts";
export function minimaxCnProvider(): Provider<"anthropic-messages"> {
return createProvider({
id: "minimax-cn",
name: "MiniMax CN",
baseUrl: "https://api.minimaxi.com/anthropic",
auth: { apiKey: envApiKeyAuth("MiniMax CN API key", ["MINIMAX_CN_API_KEY"]) },
models: Object.values(MINIMAX_CN_MODELS),
api: anthropicMessagesApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/minimax.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const MINIMAX_MODELS: ModelCatalog<typeof values, "minimax"> =
flattenModelCatalog("minimax", values);
+15
View File
@@ -0,0 +1,15 @@
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { MINIMAX_MODELS } from "./minimax.models.ts";
export function minimaxProvider(): Provider<"anthropic-messages"> {
return createProvider({
id: "minimax",
name: "MiniMax",
baseUrl: "https://api.minimax.io/anthropic",
auth: { apiKey: envApiKeyAuth("MiniMax API key", ["MINIMAX_API_KEY"]) },
models: Object.values(MINIMAX_MODELS),
api: anthropicMessagesApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/mistral.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const MISTRAL_MODELS: ModelCatalog<typeof values, "mistral"> =
flattenModelCatalog("mistral", values);
+15
View File
@@ -0,0 +1,15 @@
import { mistralConversationsApi } from "../api/mistral-conversations.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { MISTRAL_MODELS } from "./mistral.models.ts";
export function mistralProvider(): Provider<"mistral-conversations"> {
return createProvider({
id: "mistral",
name: "Mistral",
baseUrl: "https://api.mistral.ai",
auth: { apiKey: envApiKeyAuth("Mistral API key", ["MISTRAL_API_KEY"]) },
models: Object.values(MISTRAL_MODELS),
api: mistralConversationsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/moonshotai-cn.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const MOONSHOTAI_CN_MODELS: ModelCatalog<typeof values, "moonshotai-cn"> =
flattenModelCatalog("moonshotai-cn", values);
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { MOONSHOTAI_CN_MODELS } from "./moonshotai-cn.models.ts";
export function moonshotaiCnProvider(): Provider<"openai-completions"> {
return createProvider({
id: "moonshotai-cn",
name: "Moonshot AI CN",
baseUrl: "https://api.moonshot.cn/v1",
auth: { apiKey: envApiKeyAuth("Moonshot AI API key", ["MOONSHOT_API_KEY"]) },
models: Object.values(MOONSHOTAI_CN_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/moonshotai.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const MOONSHOTAI_MODELS: ModelCatalog<typeof values, "moonshotai"> =
flattenModelCatalog("moonshotai", values);
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { MOONSHOTAI_MODELS } from "./moonshotai.models.ts";
export function moonshotaiProvider(): Provider<"openai-completions"> {
return createProvider({
id: "moonshotai",
name: "Moonshot AI",
baseUrl: "https://api.moonshot.ai/v1",
auth: { apiKey: envApiKeyAuth("Moonshot AI API key", ["MOONSHOT_API_KEY"]) },
models: Object.values(MOONSHOTAI_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/nvidia.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const NVIDIA_MODELS: ModelCatalog<typeof values, "nvidia"> =
flattenModelCatalog("nvidia", values);
+15
View File
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { NVIDIA_MODELS } from "./nvidia.models.ts";
export function nvidiaProvider(): Provider<"openai-completions"> {
return createProvider({
id: "nvidia",
name: "NVIDIA",
baseUrl: "https://integrate.api.nvidia.com/v1",
auth: { apiKey: envApiKeyAuth("NVIDIA API key", ["NVIDIA_API_KEY"]) },
models: Object.values(NVIDIA_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/openai-codex.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const OPENAI_CODEX_MODELS: ModelCatalog<typeof values, "openai-codex"> =
flattenModelCatalog("openai-codex", values);
@@ -0,0 +1,18 @@
import { openAICodexResponsesApi } from "../api/openai-codex-responses.lazy.ts";
import { lazyOAuth } from "../auth/helpers.ts";
import { loadOpenAICodexOAuth } from "../auth/oauth/load.ts";
import { createProvider, type Provider } from "../models.ts";
import { OPENAI_CODEX_MODELS } from "./openai-codex.models.ts";
export function openaiCodexProvider(): Provider<"openai-codex-responses"> {
return createProvider({
id: "openai-codex",
name: "OpenAI Codex",
baseUrl: "https://chatgpt.com/backend-api",
auth: {
oauth: lazyOAuth({ name: "OpenAI (ChatGPT Plus/Pro)", load: loadOpenAICodexOAuth }),
},
models: Object.values(OPENAI_CODEX_MODELS),
api: openAICodexResponsesApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/openai.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const OPENAI_MODELS: ModelCatalog<typeof values, "openai"> =
flattenModelCatalog("openai", values);
+15
View File
@@ -0,0 +1,15 @@
import { openAIResponsesApi } from "../api/openai-responses.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { OPENAI_MODELS } from "./openai.models.ts";
export function openaiProvider(): Provider<"openai-responses"> {
return createProvider({
id: "openai",
name: "OpenAI",
baseUrl: "https://api.openai.com/v1",
auth: { apiKey: envApiKeyAuth("OpenAI API key", ["OPENAI_API_KEY"]) },
models: Object.values(OPENAI_MODELS),
api: openAIResponsesApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/opencode-go.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const OPENCODE_GO_MODELS: ModelCatalog<typeof values, "opencode-go"> =
flattenModelCatalog("opencode-go", values);
@@ -0,0 +1,20 @@
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { openAIResponsesApi } from "../api/openai-responses.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { OPENCODE_GO_MODELS } from "./opencode-go.models.ts";
export function opencodeGoProvider(): Provider<"anthropic-messages" | "openai-completions" | "openai-responses"> {
return createProvider<"anthropic-messages" | "openai-completions" | "openai-responses">({
id: "opencode-go",
name: "OpenCode Go",
auth: { apiKey: envApiKeyAuth("OpenCode API key", ["OPENCODE_API_KEY"]) },
models: Object.values(OPENCODE_GO_MODELS),
api: {
"anthropic-messages": anthropicMessagesApi(),
"openai-completions": openAICompletionsApi(),
"openai-responses": openAIResponsesApi(),
},
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/opencode.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const OPENCODE_MODELS: ModelCatalog<typeof values, "opencode"> =
flattenModelCatalog("opencode", values);
+24
View File
@@ -0,0 +1,24 @@
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
import { googleGenerativeAIApi } from "../api/google-generative-ai.lazy.ts";
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { openAIResponsesApi } from "../api/openai-responses.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { OPENCODE_MODELS } from "./opencode.models.ts";
export function opencodeProvider(): Provider<
"anthropic-messages" | "google-generative-ai" | "openai-completions" | "openai-responses"
> {
return createProvider({
id: "opencode",
name: "OpenCode Zen",
auth: { apiKey: envApiKeyAuth("OpenCode API key", ["OPENCODE_API_KEY"]) },
models: Object.values(OPENCODE_MODELS),
api: {
"anthropic-messages": anthropicMessagesApi(),
"google-generative-ai": googleGenerativeAIApi(),
"openai-completions": openAICompletionsApi(),
"openai-responses": openAIResponsesApi(),
},
});
}
@@ -0,0 +1,22 @@
import { openrouterImagesApi } from "../api/openrouter-images.lazy.ts";
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
import { loadOpenRouterOAuth } from "../auth/oauth/load.ts";
import { IMAGE_MODELS } from "../image-models.generated.ts";
import { createImagesProvider, type ImagesProvider } from "../images-models.ts";
export function openrouterImagesProvider(): ImagesProvider {
return createImagesProvider({
id: "openrouter",
name: "OpenRouter",
auth: {
apiKey: envApiKeyAuth("OpenRouter API key", ["OPENROUTER_API_KEY"]),
oauth: lazyOAuth({
name: "OpenRouter OAuth",
loginLabel: "Sign in with OpenRouter",
load: loadOpenRouterOAuth,
}),
},
models: Object.values(IMAGE_MODELS.openrouter),
api: openrouterImagesApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/openrouter.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const OPENROUTER_MODELS: ModelCatalog<typeof values, "openrouter"> =
flattenModelCatalog("openrouter", values);
@@ -0,0 +1,23 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
import { loadOpenRouterOAuth } from "../auth/oauth/load.ts";
import { createProvider, type Provider } from "../models.ts";
import { OPENROUTER_MODELS } from "./openrouter.models.ts";
export function openrouterProvider(): Provider<"openai-completions"> {
return createProvider({
id: "openrouter",
name: "OpenRouter",
baseUrl: "https://openrouter.ai/api/v1",
auth: {
apiKey: envApiKeyAuth("OpenRouter API key", ["OPENROUTER_API_KEY"]),
oauth: lazyOAuth({
name: "OpenRouter OAuth",
loginLabel: "Sign in with OpenRouter",
load: loadOpenRouterOAuth,
}),
},
models: Object.values(OPENROUTER_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/qwen-token-plan-cn.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const QWEN_TOKEN_PLAN_CN_MODELS: ModelCatalog<typeof values, "qwen-token-plan-cn"> =
flattenModelCatalog("qwen-token-plan-cn", values);
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { QWEN_TOKEN_PLAN_CN_MODELS } from "./qwen-token-plan-cn.models.ts";
export function qwenTokenPlanCnProvider(): Provider<"openai-completions"> {
return createProvider({
id: "qwen-token-plan-cn",
name: "Qwen Token Plan CN",
baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
auth: { apiKey: envApiKeyAuth("Qwen Token Plan CN API key", ["QWEN_TOKEN_PLAN_CN_API_KEY"]) },
models: Object.values(QWEN_TOKEN_PLAN_CN_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/qwen-token-plan.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const QWEN_TOKEN_PLAN_MODELS: ModelCatalog<typeof values, "qwen-token-plan"> =
flattenModelCatalog("qwen-token-plan", values);
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { QWEN_TOKEN_PLAN_MODELS } from "./qwen-token-plan.models.ts";
export function qwenTokenPlanProvider(): Provider<"openai-completions"> {
return createProvider({
id: "qwen-token-plan",
name: "Qwen Token Plan",
baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
auth: { apiKey: envApiKeyAuth("Qwen Token Plan API key", ["QWEN_TOKEN_PLAN_API_KEY"]) },
models: Object.values(QWEN_TOKEN_PLAN_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,96 @@
import type { OAuthCredential } from "../auth/types.ts";
import type { Model, ThinkingLevelMap } from "../types.ts";
export const DEFAULT_RADIUS_GATEWAY = "https://radius.pi.dev";
export type RadiusGatewayModel = {
id: string;
name: string;
reasoning: boolean;
thinkingLevelMap?: ThinkingLevelMap;
input: ("text" | "image")[];
cost: Model<"pi-messages">["cost"];
contextWindow: number;
maxTokens: number;
};
export type RadiusGatewayConfig = {
baseUrl: string;
models: RadiusGatewayModel[];
};
export type RadiusOAuthCredential = OAuthCredential & {
gatewayConfig?: RadiusGatewayConfig;
};
function isRadiusGatewayModel(value: unknown): value is RadiusGatewayModel {
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
const model = value as Partial<RadiusGatewayModel>;
return (
typeof model.id === "string" &&
typeof model.name === "string" &&
typeof model.reasoning === "boolean" &&
Array.isArray(model.input) &&
typeof model.cost === "object" &&
model.cost !== null &&
!Array.isArray(model.cost) &&
typeof model.contextWindow === "number" &&
typeof model.maxTokens === "number"
);
}
function sanitizeRadiusGatewayConfig(config: unknown): RadiusGatewayConfig | undefined {
if (typeof config !== "object" || config === null || Array.isArray(config)) return undefined;
const { baseUrl, models } = config as Partial<RadiusGatewayConfig>;
if (typeof baseUrl !== "string" || !Array.isArray(models)) return undefined;
return {
baseUrl,
models: models.filter(isRadiusGatewayModel).map((model) => ({ ...model })),
};
}
export function normalizeRadiusGatewayUrl(value: string): string {
const withScheme = /^https?:\/\//iu.test(value) ? value : `https://${value}`;
return withScheme.replace(/\/+$/u, "");
}
export function getRadiusCredentialConfig(credential: OAuthCredential | undefined): RadiusGatewayConfig | undefined {
return sanitizeRadiusGatewayConfig((credential as RadiusOAuthCredential | undefined)?.gatewayConfig);
}
export function getRadiusModelsFromConfig(providerId: string, config: RadiusGatewayConfig): Model<"pi-messages">[] {
return config.models.map((model) => ({
...model,
api: "pi-messages",
provider: providerId,
baseUrl: config.baseUrl,
}));
}
export function getRadiusModels(providerId: string, credential: OAuthCredential | undefined): Model<"pi-messages">[] {
const config = getRadiusCredentialConfig(credential);
return config ? getRadiusModelsFromConfig(providerId, config) : [];
}
function truncateHttpBody(body: string): string {
const trimmed = body.trim();
return trimmed.length > 512 ? `${trimmed.slice(0, 512)}…` : trimmed;
}
export async function loadRadiusGatewayConfig(
gateway: string,
apiKey?: string,
signal?: AbortSignal,
): Promise<RadiusGatewayConfig> {
const headers: Record<string, string> = { accept: "application/json" };
if (apiKey) headers.authorization = `Bearer ${apiKey}`;
const response = await fetch(new URL("/v1/config", gateway), { headers, signal });
if (!response.ok) {
throw new Error(
`Could not load Radius config from ${gateway}: ${response.status}: ${truncateHttpBody(await response.text())}`,
);
}
const config = sanitizeRadiusGatewayConfig(await response.json());
if (!config) throw new Error(`Invalid Radius config from ${gateway}`);
return config;
}
+67
View File
@@ -0,0 +1,67 @@
import { piMessagesApi } from "../api/pi-messages.lazy.ts";
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
import { loadRadiusOAuth } from "../auth/oauth/load.ts";
import type { Provider } from "../models.ts";
import {
DEFAULT_RADIUS_GATEWAY,
getRadiusModels,
getRadiusModelsFromConfig,
loadRadiusGatewayConfig,
normalizeRadiusGatewayUrl,
} from "./radius-config.ts";
export interface RadiusProviderOptions {
id?: string;
name?: string;
gateway?: string;
}
/** Radius gateway provider with a persisted, dynamically refreshed catalog. */
export function radiusProvider(options: RadiusProviderOptions = {}): Provider<"pi-messages"> {
const id = options.id ?? "radius";
const name = options.name ?? "Radius";
const gateway = normalizeRadiusGatewayUrl(options.gateway ?? DEFAULT_RADIUS_GATEWAY);
let models = getRadiusModels(id, undefined);
let inflightRefresh: Promise<void> | undefined;
const streams = piMessagesApi();
return {
id,
name,
auth: {
apiKey: envApiKeyAuth("Radius API key", ["RADIUS_API_KEY"]),
oauth: lazyOAuth({ name, load: () => loadRadiusOAuth({ name, gateway }) }),
},
getModels: () => models,
refreshModels: (context) => {
inflightRefresh ??= (async () => {
try {
const stored = await context.store.read();
if (stored) models = stored.models.filter((model) => model.provider === id) as typeof models;
// Import catalogs cached by the pre-ModelsStore Radius implementation.
if (!stored && context.credential?.type === "oauth") {
const legacy = getRadiusModels(id, context.credential);
if (legacy.length > 0) {
models = legacy;
await context.store.write({ models: legacy, checkedAt: Date.now() });
}
}
if (!context.allowNetwork || context.signal?.aborted) return;
const apiKey =
context.credential?.type === "oauth" ? context.credential.access : context.credential?.key;
const config = await loadRadiusGatewayConfig(gateway, apiKey, context.signal);
if (context.signal?.aborted) return;
models = getRadiusModelsFromConfig(id, config);
await context.store.write({ models, checkedAt: Date.now() });
} finally {
inflightRefresh = undefined;
}
})();
return inflightRefresh;
},
stream: (model, context, streamOptions) => streams.stream(model, context, streamOptions),
streamSimple: (model, context, streamOptions) => streams.streamSimple(model, context, streamOptions),
};
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/together.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const TOGETHER_MODELS: ModelCatalog<typeof values, "together"> =
flattenModelCatalog("together", values);
+15
View File
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { TOGETHER_MODELS } from "./together.models.ts";
export function togetherProvider(): Provider<"openai-completions"> {
return createProvider({
id: "together",
name: "Together",
baseUrl: "https://api.together.ai/v1",
auth: { apiKey: envApiKeyAuth("Together API key", ["TOGETHER_API_KEY"]) },
models: Object.values(TOGETHER_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/vercel-ai-gateway.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const VERCEL_AI_GATEWAY_MODELS: ModelCatalog<typeof values, "vercel-ai-gateway"> =
flattenModelCatalog("vercel-ai-gateway", values);
@@ -0,0 +1,15 @@
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { VERCEL_AI_GATEWAY_MODELS } from "./vercel-ai-gateway.models.ts";
export function vercelAIGatewayProvider(): Provider<"anthropic-messages"> {
return createProvider({
id: "vercel-ai-gateway",
name: "Vercel AI Gateway",
baseUrl: "https://ai-gateway.vercel.sh",
auth: { apiKey: envApiKeyAuth("Vercel AI Gateway API key", ["AI_GATEWAY_API_KEY"]) },
models: Object.values(VERCEL_AI_GATEWAY_MODELS),
api: anthropicMessagesApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/xai.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const XAI_MODELS: ModelCatalog<typeof values, "xai"> =
flattenModelCatalog("xai", values);
+27
View File
@@ -0,0 +1,27 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { openAIResponsesApi } from "../api/openai-responses.lazy.ts";
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
import { loadXaiOAuth } from "../auth/oauth/load.ts";
import { createProvider, type Provider } from "../models.ts";
import { XAI_MODELS } from "./xai.models.ts";
export function xaiProvider(): Provider<"openai-completions" | "openai-responses"> {
return createProvider({
id: "xai",
name: "xAI",
baseUrl: "https://api.x.ai/v1",
auth: {
apiKey: envApiKeyAuth("xAI API key", ["XAI_API_KEY"]),
oauth: lazyOAuth({
name: "xAI (Grok/X subscription)",
loginLabel: "Sign in with SuperGrok or X Premium",
load: loadXaiOAuth,
}),
},
models: Object.values(XAI_MODELS),
api: {
"openai-completions": openAICompletionsApi(),
"openai-responses": openAIResponsesApi(),
},
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/xiaomi-token-plan-ams.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const XIAOMI_TOKEN_PLAN_AMS_MODELS: ModelCatalog<typeof values, "xiaomi-token-plan-ams"> =
flattenModelCatalog("xiaomi-token-plan-ams", values);
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { XIAOMI_TOKEN_PLAN_AMS_MODELS } from "./xiaomi-token-plan-ams.models.ts";
export function xiaomiTokenPlanAmsProvider(): Provider<"openai-completions"> {
return createProvider({
id: "xiaomi-token-plan-ams",
name: "Xiaomi Token Plan AMS",
baseUrl: "https://token-plan-ams.xiaomimimo.com/v1",
auth: { apiKey: envApiKeyAuth("Xiaomi Token Plan AMS API key", ["XIAOMI_TOKEN_PLAN_AMS_API_KEY"]) },
models: Object.values(XIAOMI_TOKEN_PLAN_AMS_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/xiaomi-token-plan-cn.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const XIAOMI_TOKEN_PLAN_CN_MODELS: ModelCatalog<typeof values, "xiaomi-token-plan-cn"> =
flattenModelCatalog("xiaomi-token-plan-cn", values);
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { XIAOMI_TOKEN_PLAN_CN_MODELS } from "./xiaomi-token-plan-cn.models.ts";
export function xiaomiTokenPlanCnProvider(): Provider<"openai-completions"> {
return createProvider({
id: "xiaomi-token-plan-cn",
name: "Xiaomi Token Plan CN",
baseUrl: "https://token-plan-cn.xiaomimimo.com/v1",
auth: { apiKey: envApiKeyAuth("Xiaomi Token Plan CN API key", ["XIAOMI_TOKEN_PLAN_CN_API_KEY"]) },
models: Object.values(XIAOMI_TOKEN_PLAN_CN_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/xiaomi-token-plan-sgp.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const XIAOMI_TOKEN_PLAN_SGP_MODELS: ModelCatalog<typeof values, "xiaomi-token-plan-sgp"> =
flattenModelCatalog("xiaomi-token-plan-sgp", values);
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { XIAOMI_TOKEN_PLAN_SGP_MODELS } from "./xiaomi-token-plan-sgp.models.ts";
export function xiaomiTokenPlanSgpProvider(): Provider<"openai-completions"> {
return createProvider({
id: "xiaomi-token-plan-sgp",
name: "Xiaomi Token Plan SGP",
baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1",
auth: { apiKey: envApiKeyAuth("Xiaomi Token Plan SGP API key", ["XIAOMI_TOKEN_PLAN_SGP_API_KEY"]) },
models: Object.values(XIAOMI_TOKEN_PLAN_SGP_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/xiaomi.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const XIAOMI_MODELS: ModelCatalog<typeof values, "xiaomi"> =
flattenModelCatalog("xiaomi", values);
+15
View File
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { XIAOMI_MODELS } from "./xiaomi.models.ts";
export function xiaomiProvider(): Provider<"openai-completions"> {
return createProvider({
id: "xiaomi",
name: "Xiaomi",
baseUrl: "https://api.xiaomimimo.com/v1",
auth: { apiKey: envApiKeyAuth("Xiaomi API key", ["XIAOMI_API_KEY"]) },
models: Object.values(XIAOMI_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/zai-coding-cn.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const ZAI_CODING_CN_MODELS: ModelCatalog<typeof values, "zai-coding-cn"> =
flattenModelCatalog("zai-coding-cn", values);
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { ZAI_CODING_CN_MODELS } from "./zai-coding-cn.models.ts";
export function zaiCodingCnProvider(): Provider<"openai-completions"> {
return createProvider({
id: "zai-coding-cn",
name: "Z.AI Coding CN",
baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
auth: { apiKey: envApiKeyAuth("Z.AI Coding CN API key", ["ZAI_CODING_CN_API_KEY"]) },
models: Object.values(ZAI_CODING_CN_MODELS),
api: openAICompletionsApi(),
});
}
@@ -0,0 +1,8 @@
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/zai.json" with { type: "json" };
import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";
export const ZAI_MODELS: ModelCatalog<typeof values, "zai"> =
flattenModelCatalog("zai", values);
+15
View File
@@ -0,0 +1,15 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { createProvider, type Provider } from "../models.ts";
import { ZAI_MODELS } from "./zai.models.ts";
export function zaiProvider(): Provider<"openai-completions"> {
return createProvider({
id: "zai",
name: "Z.AI",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
auth: { apiKey: envApiKeyAuth("Z.AI API key", ["ZAI_API_KEY"]) },
models: Object.values(ZAI_MODELS),
api: openAICompletionsApi(),
});
}