mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 11:09:34 +00:00
feat: integrate Pi backend and Pi Web
This commit is contained in:
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
@@ -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);
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user