feat(coding-agent): replace model registry with model runtime
Move provider auth and OAuth flows onto pi-ai Models, compose models.json and extension overlays through ModelRuntime, and retain ModelRegistry as an extension compatibility facade.
This commit is contained in:
+15
-10
@@ -22,13 +22,20 @@ function createSetupErrorMessage(model: Model<Api>, error: unknown): AssistantMe
|
||||
};
|
||||
}
|
||||
|
||||
function forwardStream(target: AssistantMessageEventStream, source: AsyncIterable<AssistantMessageEvent>): void {
|
||||
(async () => {
|
||||
for await (const event of source) {
|
||||
target.push(event);
|
||||
}
|
||||
target.end();
|
||||
})();
|
||||
function hasResult(
|
||||
source: AsyncIterable<AssistantMessageEvent>,
|
||||
): source is AsyncIterable<AssistantMessageEvent> & { result(): Promise<AssistantMessage> } {
|
||||
return typeof (source as { result?: unknown }).result === "function";
|
||||
}
|
||||
|
||||
async function forwardStream(
|
||||
target: AssistantMessageEventStream,
|
||||
source: AsyncIterable<AssistantMessageEvent>,
|
||||
): Promise<void> {
|
||||
for await (const event of source) {
|
||||
target.push(event);
|
||||
}
|
||||
target.end(hasResult(source) ? await source.result() : undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,9 +50,7 @@ export function lazyStream(
|
||||
const outer = new AssistantMessageEventStream();
|
||||
|
||||
setup()
|
||||
.then((inner) => {
|
||||
forwardStream(outer, inner);
|
||||
})
|
||||
.then((inner) => forwardStream(outer, inner))
|
||||
.catch((error) => {
|
||||
const message = createSetupErrorMessage(model, error);
|
||||
outer.push({ type: "error", reason: "error", error: message });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Credential, CredentialStore } from "./types.ts";
|
||||
import type { Credential, CredentialInfo, CredentialStore } from "./types.ts";
|
||||
|
||||
/**
|
||||
* Default in-memory credential store. Apps inject persistent stores.
|
||||
@@ -27,6 +27,10 @@ export class InMemoryCredentialStore implements CredentialStore {
|
||||
return this.credentials.get(providerId);
|
||||
}
|
||||
|
||||
async list(): Promise<readonly CredentialInfo[]> {
|
||||
return [...this.credentials].map(([providerId, credential]) => ({ providerId, type: credential.type }));
|
||||
}
|
||||
|
||||
modify(
|
||||
providerId: string,
|
||||
fn: (current: Credential | undefined) => Promise<Credential | undefined>,
|
||||
|
||||
@@ -9,8 +9,8 @@ import type { ApiKeyAuth, OAuthAuth } from "./types.ts";
|
||||
export function envApiKeyAuth(name: string, envVars: readonly string[]): ApiKeyAuth {
|
||||
return {
|
||||
name,
|
||||
login: async (callbacks) => {
|
||||
const key = await callbacks.prompt({ type: "secret", message: `Enter ${name}` });
|
||||
login: async (interaction) => {
|
||||
const key = await interaction.prompt({ type: "secret", message: `Enter ${name}` });
|
||||
return { type: "api_key", key };
|
||||
},
|
||||
resolve: async ({ ctx, credential }) => {
|
||||
@@ -39,7 +39,7 @@ export function lazyOAuth(input: { name: string; load: () => Promise<OAuthAuth>
|
||||
};
|
||||
return {
|
||||
name: input.name,
|
||||
login: async (callbacks) => (await loaded()).login(callbacks),
|
||||
login: async (interaction) => (await loaded()).login(interaction),
|
||||
refresh: async (credential) => (await loaded()).refresh(credential),
|
||||
toAuth: async (credential) => (await loaded()).toAuth(credential),
|
||||
};
|
||||
|
||||
+49
-139
@@ -6,11 +6,10 @@
|
||||
*/
|
||||
|
||||
import type { Server } from "node:http";
|
||||
import type { OAuthAuth } from "../../auth/types.ts";
|
||||
import { getProviderEnvValue } from "../provider-env.ts";
|
||||
import { getProviderEnvValue } from "../../utils/provider-env.ts";
|
||||
import type { AuthInteraction, OAuthAuth, OAuthCredential } from "../types.ts";
|
||||
import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts";
|
||||
import { generatePKCE } from "./pkce.ts";
|
||||
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.ts";
|
||||
|
||||
type CallbackServerInfo = {
|
||||
server: Server;
|
||||
@@ -193,7 +192,7 @@ async function exchangeAuthorizationCode(
|
||||
state: string,
|
||||
verifier: string,
|
||||
redirectUri: string,
|
||||
): Promise<OAuthCredentials> {
|
||||
): Promise<OAuthCredential> {
|
||||
let responseBody: string;
|
||||
try {
|
||||
responseBody = await postJson(TOKEN_URL, {
|
||||
@@ -220,27 +219,21 @@ async function exchangeAuthorizationCode(
|
||||
}
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
refresh: tokenData.refresh_token,
|
||||
access: tokenData.access_token,
|
||||
expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with Anthropic OAuth (authorization code + PKCE)
|
||||
*/
|
||||
export async function loginAnthropic(options: {
|
||||
onAuth: (info: { url: string; instructions?: string }) => void;
|
||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
||||
onProgress?: (message: string) => void;
|
||||
onManualCodeInput?: () => Promise<string>;
|
||||
}): Promise<OAuthCredentials> {
|
||||
async function loginAnthropic(interaction: AuthInteraction): Promise<OAuthCredential> {
|
||||
const { verifier, challenge } = await generatePKCE();
|
||||
const server = await startCallbackServer(verifier);
|
||||
|
||||
const manualAbort = new AbortController();
|
||||
let code: string | undefined;
|
||||
let state: string | undefined;
|
||||
let redirectUriForExchange = REDIRECT_URI;
|
||||
let manualInput: string | undefined;
|
||||
let manualError: Error | undefined;
|
||||
|
||||
try {
|
||||
const authParams = new URLSearchParams({
|
||||
@@ -253,93 +246,58 @@ export async function loginAnthropic(options: {
|
||||
code_challenge_method: "S256",
|
||||
state: verifier,
|
||||
});
|
||||
|
||||
options.onAuth({
|
||||
interaction.notify({
|
||||
type: "auth_url",
|
||||
url: `${AUTHORIZE_URL}?${authParams.toString()}`,
|
||||
instructions:
|
||||
"Complete login in your browser. If the browser is on another machine, paste the final redirect URL here.",
|
||||
});
|
||||
|
||||
if (options.onManualCodeInput) {
|
||||
let manualInput: string | undefined;
|
||||
let manualError: Error | undefined;
|
||||
const manualPromise = options
|
||||
.onManualCodeInput()
|
||||
.then((input) => {
|
||||
manualInput = input;
|
||||
server.cancelWait();
|
||||
})
|
||||
.catch((err) => {
|
||||
manualError = err instanceof Error ? err : new Error(String(err));
|
||||
server.cancelWait();
|
||||
});
|
||||
|
||||
const result = await server.waitForCode();
|
||||
|
||||
if (manualError) {
|
||||
throw manualError;
|
||||
}
|
||||
|
||||
if (result?.code) {
|
||||
code = result.code;
|
||||
state = result.state;
|
||||
redirectUriForExchange = REDIRECT_URI;
|
||||
} else if (manualInput) {
|
||||
const parsed = parseAuthorizationInput(manualInput);
|
||||
if (parsed.state && parsed.state !== verifier) {
|
||||
throw new Error("OAuth state mismatch");
|
||||
}
|
||||
code = parsed.code;
|
||||
state = parsed.state ?? verifier;
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
await manualPromise;
|
||||
if (manualError) {
|
||||
throw manualError;
|
||||
}
|
||||
if (manualInput) {
|
||||
const parsed = parseAuthorizationInput(manualInput);
|
||||
if (parsed.state && parsed.state !== verifier) {
|
||||
throw new Error("OAuth state mismatch");
|
||||
}
|
||||
code = parsed.code;
|
||||
state = parsed.state ?? verifier;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const result = await server.waitForCode();
|
||||
if (result?.code) {
|
||||
code = result.code;
|
||||
state = result.state;
|
||||
redirectUriForExchange = REDIRECT_URI;
|
||||
}
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
const input = await options.onPrompt({
|
||||
message: "Paste the authorization code or full redirect URL:",
|
||||
const manualPromise = interaction
|
||||
.prompt({
|
||||
type: "manual_code",
|
||||
message: "Complete login in your browser, or paste the authorization code / redirect URL here:",
|
||||
placeholder: REDIRECT_URI,
|
||||
signal: manualAbort.signal,
|
||||
})
|
||||
.then((input) => {
|
||||
manualInput = input;
|
||||
server.cancelWait();
|
||||
})
|
||||
.catch((error) => {
|
||||
manualError = error instanceof Error ? error : new Error(String(error));
|
||||
server.cancelWait();
|
||||
});
|
||||
const parsed = parseAuthorizationInput(input);
|
||||
if (parsed.state && parsed.state !== verifier) {
|
||||
throw new Error("OAuth state mismatch");
|
||||
}
|
||||
|
||||
const result = await server.waitForCode();
|
||||
if (manualError) throw manualError;
|
||||
if (result?.code) {
|
||||
code = result.code;
|
||||
state = result.state;
|
||||
} else if (manualInput) {
|
||||
const parsed = parseAuthorizationInput(manualInput);
|
||||
if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
|
||||
code = parsed.code;
|
||||
state = parsed.state ?? verifier;
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error("Missing authorization code");
|
||||
await manualPromise;
|
||||
if (manualError) throw manualError;
|
||||
if (manualInput) {
|
||||
const parsed = parseAuthorizationInput(manualInput);
|
||||
if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
|
||||
code = parsed.code;
|
||||
state = parsed.state ?? verifier;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
throw new Error("Missing OAuth state");
|
||||
}
|
||||
|
||||
options.onProgress?.("Exchanging authorization code for tokens...");
|
||||
return exchangeAuthorizationCode(code, state, verifier, redirectUriForExchange);
|
||||
if (!code) throw new Error("Missing authorization code");
|
||||
if (!state) throw new Error("Missing OAuth state");
|
||||
interaction.notify({ type: "progress", message: "Exchanging authorization code for tokens..." });
|
||||
return exchangeAuthorizationCode(code, state, verifier, REDIRECT_URI);
|
||||
} finally {
|
||||
manualAbort.abort();
|
||||
server.server.close();
|
||||
}
|
||||
}
|
||||
@@ -347,7 +305,7 @@ export async function loginAnthropic(options: {
|
||||
/**
|
||||
* Refresh Anthropic OAuth token
|
||||
*/
|
||||
export async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> {
|
||||
async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredential> {
|
||||
let responseBody: string;
|
||||
try {
|
||||
responseBody = await postJson(TOKEN_URL, {
|
||||
@@ -374,6 +332,7 @@ export async function refreshAnthropicToken(refreshToken: string): Promise<OAuth
|
||||
}
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
refresh: data.refresh_token,
|
||||
access: data.access_token,
|
||||
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
||||
@@ -382,59 +341,10 @@ export async function refreshAnthropicToken(refreshToken: string): Promise<OAuth
|
||||
|
||||
export const anthropicOAuth: OAuthAuth = {
|
||||
name: "Anthropic (Claude Pro/Max)",
|
||||
|
||||
async login(callbacks) {
|
||||
// The manual_code prompt races the local callback server; abort it once
|
||||
// the flow settles so the UI can dismiss the pending input.
|
||||
const manualAbort = new AbortController();
|
||||
try {
|
||||
const credentials = await loginAnthropic({
|
||||
onAuth: (info) => callbacks.notify({ type: "auth_url", url: info.url, instructions: info.instructions }),
|
||||
onProgress: (message) => callbacks.notify({ type: "progress", message }),
|
||||
onPrompt: (prompt) =>
|
||||
callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }),
|
||||
onManualCodeInput: () =>
|
||||
callbacks.prompt({
|
||||
type: "manual_code",
|
||||
message: "Complete login in your browser, or paste the authorization code / redirect URL here:",
|
||||
placeholder: REDIRECT_URI,
|
||||
signal: manualAbort.signal,
|
||||
}),
|
||||
});
|
||||
return { ...credentials, type: "oauth" };
|
||||
} finally {
|
||||
manualAbort.abort();
|
||||
}
|
||||
},
|
||||
|
||||
async refresh(credential) {
|
||||
return { ...(await refreshAnthropicToken(credential.refresh)), type: "oauth" };
|
||||
},
|
||||
login: loginAnthropic,
|
||||
refresh: (credential) => refreshAnthropicToken(credential.refresh),
|
||||
|
||||
async toAuth(credential) {
|
||||
return { apiKey: credential.access };
|
||||
},
|
||||
};
|
||||
|
||||
export const anthropicOAuthProvider: OAuthProviderInterface = {
|
||||
id: "anthropic",
|
||||
name: "Anthropic (Claude Pro/Max)",
|
||||
usesCallbackServer: true,
|
||||
|
||||
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
return loginAnthropic({
|
||||
onAuth: callbacks.onAuth,
|
||||
onPrompt: callbacks.onPrompt,
|
||||
onProgress: callbacks.onProgress,
|
||||
onManualCodeInput: callbacks.onManualCodeInput,
|
||||
});
|
||||
},
|
||||
|
||||
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
||||
return refreshAnthropicToken(credentials.refresh);
|
||||
},
|
||||
|
||||
getApiKey(credentials: OAuthCredentials): string {
|
||||
return credentials.access;
|
||||
},
|
||||
};
|
||||
+20
-110
@@ -2,16 +2,9 @@
|
||||
* GitHub Copilot OAuth flow
|
||||
*/
|
||||
|
||||
import type { OAuthAuth, OAuthCredential } from "../../auth/types.ts";
|
||||
import { GITHUB_COPILOT_MODELS } from "../../providers/github-copilot.models.ts";
|
||||
import type { Api, Model } from "../../types.ts";
|
||||
import type { AuthInteraction, OAuthAuth, OAuthCredential } from "../types.ts";
|
||||
import { pollOAuthDeviceCodeFlow } from "./device-code.ts";
|
||||
import type { OAuthCredentials, OAuthDeviceCodeInfo, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.ts";
|
||||
|
||||
type CopilotCredentials = OAuthCredentials & {
|
||||
enterpriseUrl?: string;
|
||||
availableModelIds: string[];
|
||||
};
|
||||
|
||||
const decode = (s: string) => atob(s);
|
||||
const CLIENT_ID = decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg=");
|
||||
@@ -44,7 +37,7 @@ type DeviceTokenErrorResponse = {
|
||||
interval?: number;
|
||||
};
|
||||
|
||||
export function normalizeDomain(input: string): string | null {
|
||||
function normalizeDomain(input: string): string | null {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
@@ -81,7 +74,7 @@ function getBaseUrlFromToken(token: string): string | null {
|
||||
return `https://${apiHost}`;
|
||||
}
|
||||
|
||||
export function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {
|
||||
function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {
|
||||
// If we have a token, extract the base URL from proxy-ep
|
||||
if (token) {
|
||||
const urlFromToken = getBaseUrlFromToken(token);
|
||||
@@ -251,7 +244,7 @@ async function pollForGitHubAccessToken(
|
||||
async function refreshGitHubCopilotAccessToken(
|
||||
refreshToken: string,
|
||||
enterpriseDomain?: string,
|
||||
): Promise<OAuthCredentials> {
|
||||
): Promise<OAuthCredential> {
|
||||
const domain = enterpriseDomain || "github.com";
|
||||
const urls = getUrls(domain);
|
||||
|
||||
@@ -275,6 +268,7 @@ async function refreshGitHubCopilotAccessToken(
|
||||
}
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
refresh: refreshToken,
|
||||
access: token,
|
||||
expires: expiresAt * 1000 - 5 * 60 * 1000,
|
||||
@@ -285,10 +279,7 @@ async function refreshGitHubCopilotAccessToken(
|
||||
/**
|
||||
* Refresh GitHub Copilot token
|
||||
*/
|
||||
export async function refreshGitHubCopilotToken(
|
||||
refreshToken: string,
|
||||
enterpriseDomain?: string,
|
||||
): Promise<OAuthCredentials> {
|
||||
async function refreshGitHubCopilotToken(refreshToken: string, enterpriseDomain?: string): Promise<OAuthCredential> {
|
||||
const credentials = await refreshGitHubCopilotAccessToken(refreshToken, enterpriseDomain);
|
||||
return {
|
||||
...credentials,
|
||||
@@ -326,68 +317,41 @@ async function enableGitHubCopilotModel(token: string, modelId: string, enterpri
|
||||
* Enable all known GitHub Copilot models that may require policy acceptance.
|
||||
* Called after successful login to ensure all models are available.
|
||||
*/
|
||||
async function enableAllGitHubCopilotModels(
|
||||
token: string,
|
||||
enterpriseDomain?: string,
|
||||
onProgress?: (model: string, success: boolean) => void,
|
||||
): Promise<void> {
|
||||
async function enableAllGitHubCopilotModels(token: string, enterpriseDomain?: string): Promise<void> {
|
||||
const models = Object.values(GITHUB_COPILOT_MODELS);
|
||||
await Promise.all(
|
||||
models.map(async (model) => {
|
||||
const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);
|
||||
onProgress?.(model.id, success);
|
||||
await enableGitHubCopilotModel(token, model.id, enterpriseDomain);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with GitHub Copilot OAuth (device code flow)
|
||||
*
|
||||
* @param options.onDeviceCode - Callback with URL and user code
|
||||
* @param options.onPrompt - Callback to prompt user for input
|
||||
* @param options.onProgress - Optional progress callback
|
||||
* @param options.signal - Optional AbortSignal for cancellation
|
||||
*/
|
||||
export async function loginGitHubCopilot(options: {
|
||||
onDeviceCode: (info: OAuthDeviceCodeInfo) => void;
|
||||
onPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;
|
||||
onProgress?: (message: string) => void;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<OAuthCredentials> {
|
||||
const input = await options.onPrompt({
|
||||
async function loginGitHubCopilot(interaction: AuthInteraction): Promise<OAuthCredential> {
|
||||
const input = await interaction.prompt({
|
||||
type: "text",
|
||||
message: "GitHub Enterprise URL/domain (blank for github.com)",
|
||||
placeholder: "company.ghe.com",
|
||||
allowEmpty: true,
|
||||
});
|
||||
|
||||
if (options.signal?.aborted) {
|
||||
throw new Error("Login cancelled");
|
||||
}
|
||||
if (interaction.signal?.aborted) throw new Error("Login cancelled");
|
||||
|
||||
const trimmed = input.trim();
|
||||
const enterpriseDomain = normalizeDomain(input);
|
||||
if (trimmed && !enterpriseDomain) {
|
||||
throw new Error("Invalid GitHub Enterprise URL/domain");
|
||||
}
|
||||
if (trimmed && !enterpriseDomain) throw new Error("Invalid GitHub Enterprise URL/domain");
|
||||
const domain = enterpriseDomain || "github.com";
|
||||
|
||||
const device = await startDeviceFlow(domain);
|
||||
options.onDeviceCode({
|
||||
interaction.notify({
|
||||
type: "device_code",
|
||||
userCode: device.user_code,
|
||||
verificationUri: device.verification_uri,
|
||||
intervalSeconds: device.interval,
|
||||
expiresInSeconds: device.expires_in,
|
||||
});
|
||||
|
||||
const githubAccessToken = await pollForGitHubAccessToken(domain, device, options.signal);
|
||||
const githubAccessToken = await pollForGitHubAccessToken(domain, device, interaction.signal);
|
||||
const credentials = await refreshGitHubCopilotAccessToken(githubAccessToken, enterpriseDomain ?? undefined);
|
||||
|
||||
// Enable all models after successful login
|
||||
options.onProgress?.("Enabling models...");
|
||||
interaction.notify({ type: "progress", message: "Enabling models..." });
|
||||
await enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined);
|
||||
|
||||
// Fetch availability after policy enable so newly enabled models are included,
|
||||
// while unavailable models are still filtered out.
|
||||
return {
|
||||
...credentials,
|
||||
availableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain ?? undefined),
|
||||
@@ -402,26 +366,10 @@ function copilotEnterpriseDomain(credential: OAuthCredential): string | undefine
|
||||
|
||||
export const githubCopilotOAuth: OAuthAuth = {
|
||||
name: "GitHub Copilot",
|
||||
login: loginGitHubCopilot,
|
||||
refresh: (credential) => refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential)),
|
||||
|
||||
async login(callbacks) {
|
||||
const credentials = await loginGitHubCopilot({
|
||||
onDeviceCode: (info) => callbacks.notify({ type: "device_code", ...info }),
|
||||
onPrompt: (prompt) =>
|
||||
callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }),
|
||||
onProgress: (message) => callbacks.notify({ type: "progress", message }),
|
||||
signal: callbacks.signal,
|
||||
});
|
||||
return { ...credentials, type: "oauth" };
|
||||
},
|
||||
|
||||
async refresh(credential) {
|
||||
return {
|
||||
...(await refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential))),
|
||||
type: "oauth",
|
||||
};
|
||||
},
|
||||
|
||||
/** Per-credential baseUrl from the token's proxy endpoint replaces the old `modifyModels` rewriting. */
|
||||
/** Derive the credential-specific proxy endpoint for each request. */
|
||||
async toAuth(credential) {
|
||||
return {
|
||||
apiKey: credential.access,
|
||||
@@ -429,41 +377,3 @@ export const githubCopilotOAuth: OAuthAuth = {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const githubCopilotOAuthProvider: OAuthProviderInterface = {
|
||||
id: "github-copilot",
|
||||
name: "GitHub Copilot",
|
||||
|
||||
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
return loginGitHubCopilot({
|
||||
onDeviceCode: callbacks.onDeviceCode,
|
||||
onPrompt: callbacks.onPrompt,
|
||||
onProgress: callbacks.onProgress,
|
||||
signal: callbacks.signal,
|
||||
});
|
||||
},
|
||||
|
||||
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
||||
const creds = credentials as CopilotCredentials;
|
||||
return refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl);
|
||||
},
|
||||
|
||||
getApiKey(credentials: OAuthCredentials): string {
|
||||
return credentials.access;
|
||||
},
|
||||
|
||||
modifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {
|
||||
const creds = credentials as CopilotCredentials;
|
||||
const domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined;
|
||||
const baseUrl = getGitHubCopilotBaseUrl(creds.access, domain);
|
||||
// Older stored Pi auth entries do not have account-specific model IDs yet;
|
||||
// keep their existing generated-catalog behavior until the next refresh/login.
|
||||
const availableModelIds = "availableModelIds" in creds ? new Set(creds.availableModelIds) : undefined;
|
||||
|
||||
return models.flatMap((m) => {
|
||||
if (m.provider !== "github-copilot") return [m];
|
||||
if (availableModelIds && !availableModelIds.has(m.id)) return [];
|
||||
return [{ ...m, baseUrl }];
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OAuthAuth } from "../../auth/types.ts";
|
||||
import type { OAuthAuth } from "../types.ts";
|
||||
|
||||
/**
|
||||
* Loads an OAuth flow module through a variable specifier so bundlers cannot
|
||||
+63
-189
@@ -17,18 +17,11 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
|
||||
});
|
||||
}
|
||||
|
||||
import type { OAuthAuth } from "../../auth/types.ts";
|
||||
import { getProviderEnvValue } from "../provider-env.ts";
|
||||
import { getProviderEnvValue } from "../../utils/provider-env.ts";
|
||||
import type { AuthInteraction, OAuthAuth, OAuthCredential } from "../types.ts";
|
||||
import { pollOAuthDeviceCodeFlow } from "./device-code.ts";
|
||||
import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts";
|
||||
import { generatePKCE } from "./pkce.ts";
|
||||
import type {
|
||||
OAuthCredentials,
|
||||
OAuthDeviceCodeInfo,
|
||||
OAuthLoginCallbacks,
|
||||
OAuthPrompt,
|
||||
OAuthProviderInterface,
|
||||
} from "./types.ts";
|
||||
|
||||
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
||||
const AUTH_BASE_URL = "https://auth.openai.com";
|
||||
@@ -40,8 +33,8 @@ const DEVICE_TOKEN_URL = `${AUTH_BASE_URL}/api/accounts/deviceauth/token`;
|
||||
const DEVICE_VERIFICATION_URI = `${AUTH_BASE_URL}/codex/device`;
|
||||
const DEVICE_REDIRECT_URI = `${AUTH_BASE_URL}/deviceauth/callback`;
|
||||
const DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;
|
||||
export const OPENAI_CODEX_BROWSER_LOGIN_METHOD = "browser";
|
||||
export const OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD = "device_code";
|
||||
const OPENAI_CODEX_BROWSER_LOGIN_METHOD = "browser";
|
||||
const OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD = "device_code";
|
||||
const SCOPE = "openid profile email offline_access";
|
||||
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
||||
|
||||
@@ -406,13 +399,14 @@ function getAccountId(accessToken: string): string | null {
|
||||
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
||||
}
|
||||
|
||||
function credentialsFromToken(token: OAuthToken): OAuthCredentials {
|
||||
function credentialsFromToken(token: OAuthToken): OAuthCredential {
|
||||
const accountId = getAccountId(token.access);
|
||||
if (!accountId) {
|
||||
throw new Error("Failed to extract accountId from token");
|
||||
}
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
access: token.access,
|
||||
refresh: token.refresh,
|
||||
expires: token.expires,
|
||||
@@ -425,132 +419,83 @@ async function exchangeAuthorizationCodeForCredentials(
|
||||
verifier: string,
|
||||
redirectUri: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<OAuthCredentials> {
|
||||
): Promise<OAuthCredential> {
|
||||
return credentialsFromToken(await exchangeAuthorizationCode(code, verifier, redirectUri, signal));
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with OpenAI Codex OAuth using the Codex device-code flow.
|
||||
*/
|
||||
export async function loginOpenAICodexDeviceCode(options: {
|
||||
onDeviceCode: (info: OAuthDeviceCodeInfo) => void;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<OAuthCredentials> {
|
||||
const device = await startOpenAICodexDeviceAuth(options.signal);
|
||||
options.onDeviceCode({
|
||||
async function loginOpenAICodexDeviceCode(interaction: AuthInteraction): Promise<OAuthCredential> {
|
||||
const device = await startOpenAICodexDeviceAuth(interaction.signal);
|
||||
interaction.notify({
|
||||
type: "device_code",
|
||||
userCode: device.userCode,
|
||||
verificationUri: DEVICE_VERIFICATION_URI,
|
||||
intervalSeconds: device.intervalSeconds,
|
||||
expiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,
|
||||
});
|
||||
const code = await pollOpenAICodexDeviceAuth(device, options.signal);
|
||||
const code = await pollOpenAICodexDeviceAuth(device, interaction.signal);
|
||||
return exchangeAuthorizationCodeForCredentials(
|
||||
code.authorizationCode,
|
||||
code.codeVerifier,
|
||||
DEVICE_REDIRECT_URI,
|
||||
options.signal,
|
||||
interaction.signal,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with OpenAI Codex OAuth
|
||||
*
|
||||
* @param options.onAuth - Called with URL and instructions when auth starts
|
||||
* @param options.onPrompt - Called to prompt user for manual code paste (fallback if no onManualCodeInput)
|
||||
* @param options.onProgress - Optional progress messages
|
||||
* @param options.onManualCodeInput - Optional promise that resolves with user-pasted code.
|
||||
* Races with browser callback - whichever completes first wins.
|
||||
* Useful for showing paste input immediately alongside browser flow.
|
||||
* @param options.originator - OAuth originator parameter (defaults to "pi")
|
||||
*/
|
||||
export async function loginOpenAICodex(options: {
|
||||
onAuth: (info: { url: string; instructions?: string }) => void;
|
||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
||||
onProgress?: (message: string) => void;
|
||||
onManualCodeInput?: () => Promise<string>;
|
||||
originator?: string;
|
||||
}): Promise<OAuthCredentials> {
|
||||
const { verifier, state, url } = await createAuthorizationFlow(options.originator);
|
||||
async function loginOpenAICodex(interaction: AuthInteraction): Promise<OAuthCredential> {
|
||||
const { verifier, state, url } = await createAuthorizationFlow();
|
||||
const server = await startLocalOAuthServer(state);
|
||||
|
||||
options.onAuth({ url, instructions: "A browser window should open. Complete login to finish." });
|
||||
|
||||
const manualAbort = new AbortController();
|
||||
let code: string | undefined;
|
||||
let manualCode: string | undefined;
|
||||
let manualError: Error | undefined;
|
||||
|
||||
interaction.notify({
|
||||
type: "auth_url",
|
||||
url,
|
||||
instructions: "A browser window should open. Complete login to finish.",
|
||||
});
|
||||
|
||||
try {
|
||||
if (options.onManualCodeInput) {
|
||||
// Race between browser callback and manual input
|
||||
let manualCode: string | undefined;
|
||||
let manualError: Error | undefined;
|
||||
const manualPromise = options
|
||||
.onManualCodeInput()
|
||||
.then((input) => {
|
||||
manualCode = input;
|
||||
server.cancelWait();
|
||||
})
|
||||
.catch((err) => {
|
||||
manualError = err instanceof Error ? err : new Error(String(err));
|
||||
server.cancelWait();
|
||||
});
|
||||
|
||||
const result = await server.waitForCode();
|
||||
|
||||
// If manual input was cancelled, throw that error
|
||||
if (manualError) {
|
||||
throw manualError;
|
||||
}
|
||||
|
||||
if (result?.code) {
|
||||
// Browser callback won
|
||||
code = result.code;
|
||||
} else if (manualCode) {
|
||||
// Manual input won (or callback timed out and user had entered code)
|
||||
const parsed = parseAuthorizationInput(manualCode);
|
||||
if (parsed.state && parsed.state !== state) {
|
||||
throw new Error("State mismatch");
|
||||
}
|
||||
code = parsed.code;
|
||||
}
|
||||
|
||||
// If still no code, wait for manual promise to complete and try that
|
||||
if (!code) {
|
||||
await manualPromise;
|
||||
if (manualError) {
|
||||
throw manualError;
|
||||
}
|
||||
if (manualCode) {
|
||||
const parsed = parseAuthorizationInput(manualCode);
|
||||
if (parsed.state && parsed.state !== state) {
|
||||
throw new Error("State mismatch");
|
||||
}
|
||||
code = parsed.code;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Original flow: wait for callback, then prompt if needed
|
||||
const result = await server.waitForCode();
|
||||
if (result?.code) {
|
||||
code = result.code;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to onPrompt if still no code
|
||||
if (!code) {
|
||||
const input = await options.onPrompt({
|
||||
message: "Paste the authorization code (or full redirect URL):",
|
||||
const manualPromise = interaction
|
||||
.prompt({
|
||||
type: "manual_code",
|
||||
message: "Complete login in your browser, or paste the authorization code / redirect URL here:",
|
||||
placeholder: REDIRECT_URI,
|
||||
signal: manualAbort.signal,
|
||||
})
|
||||
.then((input) => {
|
||||
manualCode = input;
|
||||
server.cancelWait();
|
||||
})
|
||||
.catch((error) => {
|
||||
manualError = error instanceof Error ? error : new Error(String(error));
|
||||
server.cancelWait();
|
||||
});
|
||||
const parsed = parseAuthorizationInput(input);
|
||||
if (parsed.state && parsed.state !== state) {
|
||||
throw new Error("State mismatch");
|
||||
}
|
||||
|
||||
const result = await server.waitForCode();
|
||||
if (manualError) throw manualError;
|
||||
if (result?.code) {
|
||||
code = result.code;
|
||||
} else if (manualCode) {
|
||||
const parsed = parseAuthorizationInput(manualCode);
|
||||
if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
|
||||
code = parsed.code;
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error("Missing authorization code");
|
||||
await manualPromise;
|
||||
if (manualError) throw manualError;
|
||||
if (manualCode) {
|
||||
const parsed = parseAuthorizationInput(manualCode);
|
||||
if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
|
||||
code = parsed.code;
|
||||
}
|
||||
}
|
||||
|
||||
return exchangeAuthorizationCodeForCredentials(code, verifier, REDIRECT_URI);
|
||||
if (!code) throw new Error("Missing authorization code");
|
||||
return exchangeAuthorizationCodeForCredentials(code, verifier, REDIRECT_URI, interaction.signal);
|
||||
} finally {
|
||||
manualAbort.abort();
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
@@ -558,15 +503,15 @@ export async function loginOpenAICodex(options: {
|
||||
/**
|
||||
* Refresh OpenAI Codex OAuth token
|
||||
*/
|
||||
export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAuthCredentials> {
|
||||
async function refreshOpenAICodexToken(refreshToken: string): Promise<OAuthCredential> {
|
||||
return credentialsFromToken(await refreshAccessToken(refreshToken));
|
||||
}
|
||||
|
||||
export const openaiCodexOAuth: OAuthAuth = {
|
||||
name: "OpenAI (ChatGPT Plus/Pro)",
|
||||
|
||||
async login(callbacks) {
|
||||
const method = await callbacks.prompt({
|
||||
async login(interaction) {
|
||||
const method = await interaction.prompt({
|
||||
type: "select",
|
||||
message: "Select OpenAI Codex login method:",
|
||||
options: [
|
||||
@@ -576,89 +521,18 @@ export const openaiCodexOAuth: OAuthAuth = {
|
||||
});
|
||||
|
||||
if (method === OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD) {
|
||||
const credentials = await loginOpenAICodexDeviceCode({
|
||||
onDeviceCode: (info) => callbacks.notify({ type: "device_code", ...info }),
|
||||
signal: callbacks.signal,
|
||||
});
|
||||
return { ...credentials, type: "oauth" };
|
||||
return loginOpenAICodexDeviceCode(interaction);
|
||||
}
|
||||
if (method !== OPENAI_CODEX_BROWSER_LOGIN_METHOD) {
|
||||
throw new Error(`Unknown OpenAI Codex login method: ${method}`);
|
||||
}
|
||||
|
||||
// The manual_code prompt races the local callback server; abort it once
|
||||
// the flow settles so the UI can dismiss the pending input.
|
||||
const manualAbort = new AbortController();
|
||||
try {
|
||||
const credentials = await loginOpenAICodex({
|
||||
onAuth: (info) => callbacks.notify({ type: "auth_url", url: info.url, instructions: info.instructions }),
|
||||
onProgress: (message) => callbacks.notify({ type: "progress", message }),
|
||||
onPrompt: (prompt) =>
|
||||
callbacks.prompt({ type: "text", message: prompt.message, placeholder: prompt.placeholder }),
|
||||
onManualCodeInput: () =>
|
||||
callbacks.prompt({
|
||||
type: "manual_code",
|
||||
message: "Complete login in your browser, or paste the authorization code / redirect URL here:",
|
||||
placeholder: REDIRECT_URI,
|
||||
signal: manualAbort.signal,
|
||||
}),
|
||||
});
|
||||
return { ...credentials, type: "oauth" };
|
||||
} finally {
|
||||
manualAbort.abort();
|
||||
}
|
||||
return loginOpenAICodex(interaction);
|
||||
},
|
||||
|
||||
async refresh(credential) {
|
||||
return { ...(await refreshOpenAICodexToken(credential.refresh)), type: "oauth" };
|
||||
},
|
||||
refresh: (credential) => refreshOpenAICodexToken(credential.refresh),
|
||||
|
||||
async toAuth(credential) {
|
||||
return { apiKey: credential.access };
|
||||
},
|
||||
};
|
||||
|
||||
export const openaiCodexOAuthProvider: OAuthProviderInterface = {
|
||||
id: "openai-codex",
|
||||
name: "ChatGPT Plus/Pro (Codex Subscription)",
|
||||
usesCallbackServer: true,
|
||||
|
||||
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
const loginMethod = await callbacks.onSelect({
|
||||
message: "Select OpenAI Codex login method:",
|
||||
options: [
|
||||
{ id: OPENAI_CODEX_BROWSER_LOGIN_METHOD, label: "Browser login (default)" },
|
||||
{ id: OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD, label: "Device code login (headless)" },
|
||||
],
|
||||
});
|
||||
if (!loginMethod) {
|
||||
throw new Error("Login cancelled");
|
||||
}
|
||||
|
||||
if (loginMethod === OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD) {
|
||||
return loginOpenAICodexDeviceCode({
|
||||
onDeviceCode: callbacks.onDeviceCode,
|
||||
signal: callbacks.signal,
|
||||
});
|
||||
}
|
||||
|
||||
if (loginMethod !== OPENAI_CODEX_BROWSER_LOGIN_METHOD) {
|
||||
throw new Error(`Unknown OpenAI Codex login method: ${loginMethod}`);
|
||||
}
|
||||
|
||||
return loginOpenAICodex({
|
||||
onAuth: callbacks.onAuth,
|
||||
onPrompt: callbacks.onPrompt,
|
||||
onProgress: callbacks.onProgress,
|
||||
onManualCodeInput: callbacks.onManualCodeInput,
|
||||
});
|
||||
},
|
||||
|
||||
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
||||
return refreshOpenAICodexToken(credentials.refresh);
|
||||
},
|
||||
|
||||
getApiKey(credentials: OAuthCredentials): string {
|
||||
return credentials.access;
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv } from "../types.ts";
|
||||
import type { ProviderEnv } from "../types.ts";
|
||||
import type {
|
||||
ApiKeyAuth,
|
||||
ApiKeyCredential,
|
||||
@@ -28,9 +28,6 @@ export class ModelsError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Model shape auth resolution receives: chat or image-generation models. */
|
||||
export type AuthModel = Model<Api> | ImagesModel<ImagesApi>;
|
||||
|
||||
/**
|
||||
* Auth resolution shared by the `Models` and `ImagesModels` collections.
|
||||
* A stored credential owns the provider: ambient/env is consulted only when
|
||||
@@ -39,7 +36,6 @@ export type AuthModel = Model<Api> | ImagesModel<ImagesApi>;
|
||||
*/
|
||||
export async function resolveProviderAuth(
|
||||
provider: { id: string; auth: ProviderAuth },
|
||||
model: AuthModel,
|
||||
credentials: CredentialStore,
|
||||
authContext: AuthContext,
|
||||
overrides?: AuthResolutionOverrides,
|
||||
@@ -47,7 +43,7 @@ export async function resolveProviderAuth(
|
||||
const requestAuthContext = overrides?.env ? overlayEnvAuthContext(authContext, overrides.env) : authContext;
|
||||
|
||||
if (overrides?.apiKey !== undefined && provider.auth.apiKey) {
|
||||
return resolveApiKey(requestAuthContext, provider.auth.apiKey, model, {
|
||||
return resolveApiKey(requestAuthContext, provider.auth.apiKey, provider.id, {
|
||||
type: "api_key",
|
||||
key: overrides.apiKey,
|
||||
env: overrides.env,
|
||||
@@ -61,13 +57,15 @@ export async function resolveProviderAuth(
|
||||
}
|
||||
if (stored.type === "api_key" && provider.auth.apiKey) {
|
||||
const credential = overrides?.env ? { ...stored, env: { ...stored.env, ...overrides.env } } : stored;
|
||||
return resolveApiKey(requestAuthContext, provider.auth.apiKey, model, credential);
|
||||
return resolveApiKey(requestAuthContext, provider.auth.apiKey, provider.id, credential);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Ambient (env vars, AWS profiles, ADC files).
|
||||
return provider.auth.apiKey ? resolveApiKey(requestAuthContext, provider.auth.apiKey, model, undefined) : undefined;
|
||||
return provider.auth.apiKey
|
||||
? resolveApiKey(requestAuthContext, provider.auth.apiKey, provider.id, undefined)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function overlayEnvAuthContext(base: AuthContext, env: ProviderEnv): AuthContext {
|
||||
@@ -122,13 +120,13 @@ async function resolveStoredOAuth(
|
||||
async function resolveApiKey(
|
||||
authContext: AuthContext,
|
||||
apiKey: ApiKeyAuth,
|
||||
model: AuthModel,
|
||||
providerId: string,
|
||||
credential: ApiKeyCredential | undefined,
|
||||
): Promise<AuthResult | undefined> {
|
||||
try {
|
||||
return await apiKey.resolve({ model, ctx: authContext, credential });
|
||||
return await apiKey.resolve({ ctx: authContext, credential });
|
||||
} catch (error) {
|
||||
throw new ModelsError("auth", `API key auth failed for provider ${model.provider}`, { cause: error });
|
||||
throw new ModelsError("auth", `API key auth failed for provider ${providerId}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv, ProviderHeaders } from "../types.ts";
|
||||
import type { OAuthCredentials } from "../utils/oauth/types.ts";
|
||||
import type { ProviderEnv, ProviderHeaders } from "../types.ts";
|
||||
|
||||
/**
|
||||
* Request auth for a single model request. If a value cannot be expressed as
|
||||
@@ -21,7 +20,15 @@ export interface ApiKeyCredential {
|
||||
env?: ProviderEnv;
|
||||
}
|
||||
|
||||
/** Stored OAuth credential (`access`, `refresh`, `expires` from OAuthCredentials). */
|
||||
/** OAuth token data returned by extension compatibility flows. */
|
||||
export interface OAuthCredentials {
|
||||
refresh: string;
|
||||
access: string;
|
||||
expires: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** Stored canonical OAuth credential. */
|
||||
export interface OAuthCredential extends OAuthCredentials {
|
||||
type: "oauth";
|
||||
}
|
||||
@@ -29,6 +36,12 @@ export interface OAuthCredential extends OAuthCredentials {
|
||||
/** One type-tagged credential per provider — the shape of today's auth.json. */
|
||||
export type Credential = ApiKeyCredential | OAuthCredential;
|
||||
|
||||
/** Non-secret credential metadata for account/status enumeration. */
|
||||
export interface CredentialInfo {
|
||||
providerId: string;
|
||||
type: Credential["type"];
|
||||
}
|
||||
|
||||
/**
|
||||
* App-owned credential storage, keyed by `Provider.id`, one credential per
|
||||
* provider. `modify` is the only write path, so every mutation is a
|
||||
@@ -51,6 +64,12 @@ export interface CredentialStore {
|
||||
*/
|
||||
read(providerId: string): Promise<Credential | undefined>;
|
||||
|
||||
/**
|
||||
* List stored credential metadata without resolving or exposing secrets.
|
||||
* Implementations must not execute configured API-key commands while listing.
|
||||
*/
|
||||
list(): Promise<readonly CredentialInfo[]>;
|
||||
|
||||
/**
|
||||
* Serialized write — the only write path. `fn` sees the current credential
|
||||
* because correct writes (refresh, login-during-refresh) depend on it;
|
||||
@@ -84,6 +103,13 @@ export interface AuthResult {
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface AuthCheck {
|
||||
source?: string;
|
||||
type: "api_key" | "oauth";
|
||||
}
|
||||
|
||||
export type AuthType = "api_key" | "oauth";
|
||||
|
||||
/**
|
||||
* Prompt shown to the user during login. `signal` lets the flow cancel a
|
||||
* pending prompt when an out-of-band event resolves the step, e.g. a
|
||||
@@ -97,7 +123,13 @@ export type AuthPrompt = { signal?: AbortSignal } & (
|
||||
| { type: "manual_code"; message: string; placeholder?: string }
|
||||
);
|
||||
|
||||
export interface AuthInfoLink {
|
||||
url: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export type AuthEvent =
|
||||
| { type: "info"; message: string; links?: readonly AuthInfoLink[] }
|
||||
| { type: "auth_url"; url: string; instructions?: string }
|
||||
| {
|
||||
type: "device_code";
|
||||
@@ -115,7 +147,7 @@ export type AuthEvent =
|
||||
* id). Rejects on cancel/abort. `signal` aborts the whole login flow;
|
||||
* per-prompt cancellation uses `AuthPrompt.signal`.
|
||||
*/
|
||||
export interface AuthLoginCallbacks {
|
||||
export interface AuthInteraction {
|
||||
signal?: AbortSignal;
|
||||
|
||||
prompt(prompt: AuthPrompt): Promise<string>;
|
||||
@@ -131,19 +163,22 @@ export interface ApiKeyAuth {
|
||||
name: string;
|
||||
|
||||
/** Interactive setup (prompt for key/provider env). Absent = ambient-only. */
|
||||
login?(callbacks: AuthLoginCallbacks): Promise<ApiKeyCredential>;
|
||||
login?(interaction: AuthInteraction): Promise<ApiKeyCredential>;
|
||||
|
||||
/**
|
||||
* Optional side-effect-free availability check. Use this when `resolve()` may
|
||||
* execute commands or perform other request-time work. Missing means Models
|
||||
* checks availability by resolving auth.
|
||||
*/
|
||||
check?(input: { ctx: AuthContext; credential?: ApiKeyCredential }): Promise<AuthCheck | undefined>;
|
||||
|
||||
/**
|
||||
* Resolve auth from the stored credential and/or ambient sources, merging
|
||||
* per field (`credential.key ?? env("...")`, `credential.env?.NAME ?? env("...")`).
|
||||
* undefined = not configured. Receives the chat or image-generation model
|
||||
* the request is for (both carry `provider` and `baseUrl`).
|
||||
* undefined = not configured. Resolution is provider-scoped; model-specific
|
||||
* endpoint preparation happens after auth has been resolved.
|
||||
*/
|
||||
resolve(input: {
|
||||
model: Model<Api> | ImagesModel<ImagesApi>;
|
||||
ctx: AuthContext;
|
||||
credential?: ApiKeyCredential;
|
||||
}): Promise<AuthResult | undefined>;
|
||||
resolve(input: { ctx: AuthContext; credential?: ApiKeyCredential }): Promise<AuthResult | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,7 +190,7 @@ export interface OAuthAuth {
|
||||
/** Display name, e.g. "Anthropic (Claude Pro/Max)". */
|
||||
name: string;
|
||||
|
||||
login(callbacks: AuthLoginCallbacks): Promise<OAuthCredential>;
|
||||
login(interaction: AuthInteraction): Promise<OAuthCredential>;
|
||||
|
||||
/**
|
||||
* Exchange the refresh token. Network call; throws on failure
|
||||
|
||||
+63
-92
@@ -1,71 +1,74 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { createInterface } from "node:readline";
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { getOAuthProvider, getOAuthProviders } from "./utils/oauth/index.ts";
|
||||
import type { OAuthCredentials, OAuthProviderId } from "./utils/oauth/types.ts";
|
||||
import type { AuthPrompt, OAuthCredential, Provider } from "./index.ts";
|
||||
import { builtinProviders } from "./providers/all.ts";
|
||||
|
||||
const AUTH_FILE = "auth.json";
|
||||
const PROVIDERS = getOAuthProviders();
|
||||
const PROVIDERS = builtinProviders().filter(
|
||||
(provider): provider is Provider & { auth: { oauth: NonNullable<Provider["auth"]["oauth"]> } } =>
|
||||
provider.auth.oauth !== undefined,
|
||||
);
|
||||
|
||||
function prompt(rl: ReturnType<typeof createInterface>, question: string): Promise<string> {
|
||||
return new Promise((resolve) => rl.question(question, resolve));
|
||||
}
|
||||
|
||||
function loadAuth(): Record<string, { type: "oauth" } & OAuthCredentials> {
|
||||
function loadAuth(): Record<string, OAuthCredential> {
|
||||
if (!existsSync(AUTH_FILE)) return {};
|
||||
try {
|
||||
return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
|
||||
return JSON.parse(readFileSync(AUTH_FILE, "utf-8")) as Record<string, OAuthCredential>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveAuth(auth: Record<string, { type: "oauth" } & OAuthCredentials>): void {
|
||||
function saveAuth(auth: Record<string, OAuthCredential>): void {
|
||||
writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
async function login(providerId: OAuthProviderId): Promise<void> {
|
||||
const provider = getOAuthProvider(providerId);
|
||||
if (!provider) {
|
||||
console.error(`Unknown provider: ${providerId}`);
|
||||
process.exit(1);
|
||||
async function answerPrompt(rl: ReturnType<typeof createInterface>, authPrompt: AuthPrompt): Promise<string> {
|
||||
if (authPrompt.type === "select") {
|
||||
console.log(`\n${authPrompt.message}`);
|
||||
for (let index = 0; index < authPrompt.options.length; index++) {
|
||||
console.log(` ${index + 1}. ${authPrompt.options[index].label}`);
|
||||
}
|
||||
const choice = Number.parseInt(await prompt(rl, `Enter number (1-${authPrompt.options.length}): `), 10) - 1;
|
||||
const selected = authPrompt.options[choice];
|
||||
if (!selected) throw new Error("Invalid selection");
|
||||
return selected.id;
|
||||
}
|
||||
return prompt(rl, `${authPrompt.message}${authPrompt.placeholder ? ` (${authPrompt.placeholder})` : ""}: `);
|
||||
}
|
||||
|
||||
async function login(providerId: string): Promise<void> {
|
||||
const provider = PROVIDERS.find((entry) => entry.id === providerId);
|
||||
if (!provider) throw new Error(`Unknown provider: ${providerId}`);
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
const promptFn = (msg: string) => prompt(rl, `${msg} `);
|
||||
|
||||
try {
|
||||
const credentials = await provider.login({
|
||||
onAuth: (info) => {
|
||||
console.log(`\nOpen this URL in your browser:\n${info.url}`);
|
||||
if (info.instructions) console.log(info.instructions);
|
||||
console.log();
|
||||
},
|
||||
onDeviceCode: (info) => {
|
||||
console.log(`\nOpen this URL in your browser:\n${info.verificationUri}`);
|
||||
console.log(`Enter code: ${info.userCode}`);
|
||||
console.log();
|
||||
},
|
||||
onPrompt: async (p) => {
|
||||
return await promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
|
||||
},
|
||||
onSelect: async (p) => {
|
||||
console.log(`\n${p.message}`);
|
||||
for (let i = 0; i < p.options.length; i++) {
|
||||
console.log(` ${i + 1}. ${p.options[i].label}`);
|
||||
const credential = await provider.auth.oauth.login({
|
||||
prompt: (authPrompt) => answerPrompt(rl, authPrompt),
|
||||
notify: (event) => {
|
||||
switch (event.type) {
|
||||
case "auth_url":
|
||||
console.log(`\nOpen this URL in your browser:\n${event.url}`);
|
||||
if (event.instructions) console.log(event.instructions);
|
||||
break;
|
||||
case "device_code":
|
||||
console.log(`\nOpen this URL in your browser:\n${event.verificationUri}`);
|
||||
console.log(`Enter code: ${event.userCode}`);
|
||||
break;
|
||||
case "info":
|
||||
case "progress":
|
||||
console.log(event.message);
|
||||
break;
|
||||
}
|
||||
const choice = await promptFn(`Enter number (1-${p.options.length}):`);
|
||||
const index = parseInt(choice, 10) - 1;
|
||||
return p.options[index]?.id;
|
||||
},
|
||||
onProgress: (msg) => console.log(msg),
|
||||
});
|
||||
|
||||
const auth = loadAuth();
|
||||
auth[providerId] = { type: "oauth", ...credentials };
|
||||
auth[providerId] = credential;
|
||||
saveAuth(auth);
|
||||
|
||||
console.log(`\nCredentials saved to ${AUTH_FILE}`);
|
||||
} finally {
|
||||
rl.close();
|
||||
@@ -75,73 +78,41 @@ async function login(providerId: OAuthProviderId): Promise<void> {
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
if (!command || command === "help" || command === "--help" || command === "-h") {
|
||||
const providerList = PROVIDERS.map((p) => ` ${p.id.padEnd(20)} ${p.name}`).join("\n");
|
||||
console.log(`Usage: npx @earendil-works/pi-ai <command> [provider]
|
||||
|
||||
Commands:
|
||||
login [provider] Login to an OAuth provider
|
||||
list List available providers
|
||||
|
||||
Providers:
|
||||
${providerList}
|
||||
|
||||
Examples:
|
||||
npx @earendil-works/pi-ai login # interactive provider selection
|
||||
npx @earendil-works/pi-ai login anthropic # login to specific provider
|
||||
npx @earendil-works/pi-ai list # list providers
|
||||
`);
|
||||
const providerList = PROVIDERS.map((provider) => ` ${provider.id.padEnd(20)} ${provider.name}`).join("\n");
|
||||
console.log(
|
||||
`Usage: npx @earendil-works/pi-ai <command> [provider]\n\nCommands:\n login [provider] Login to an OAuth provider\n list List available providers\n\nProviders:\n${providerList}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "list") {
|
||||
console.log("Available OAuth providers:\n");
|
||||
for (const p of PROVIDERS) {
|
||||
console.log(` ${p.id.padEnd(20)} ${p.name}`);
|
||||
}
|
||||
for (const provider of PROVIDERS) console.log(`${provider.id.padEnd(20)} ${provider.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "login") {
|
||||
let provider = args[1] as OAuthProviderId | undefined;
|
||||
|
||||
if (!provider) {
|
||||
let providerId = args[1];
|
||||
if (!providerId) {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
console.log("Select a provider:\n");
|
||||
for (let i = 0; i < PROVIDERS.length; i++) {
|
||||
console.log(` ${i + 1}. ${PROVIDERS[i].name}`);
|
||||
try {
|
||||
for (let index = 0; index < PROVIDERS.length; index++) {
|
||||
console.log(` ${index + 1}. ${PROVIDERS[index].name}`);
|
||||
}
|
||||
const index = Number.parseInt(await prompt(rl, `Enter number (1-${PROVIDERS.length}): `), 10) - 1;
|
||||
providerId = PROVIDERS[index]?.id;
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
console.log();
|
||||
|
||||
const choice = await prompt(rl, `Enter number (1-${PROVIDERS.length}): `);
|
||||
rl.close();
|
||||
|
||||
const index = parseInt(choice, 10) - 1;
|
||||
if (index < 0 || index >= PROVIDERS.length) {
|
||||
console.error("Invalid selection");
|
||||
process.exit(1);
|
||||
}
|
||||
provider = PROVIDERS[index].id;
|
||||
}
|
||||
|
||||
if (!PROVIDERS.some((p) => p.id === provider)) {
|
||||
console.error(`Unknown provider: ${provider}`);
|
||||
console.error(`Use 'npx @earendil-works/pi-ai list' to see available providers`);
|
||||
process.exit(1);
|
||||
if (!providerId || !PROVIDERS.some((provider) => provider.id === providerId)) {
|
||||
throw new Error(`Unknown provider: ${providerId ?? ""}`);
|
||||
}
|
||||
|
||||
console.log(`Logging in to ${provider}...`);
|
||||
await login(provider);
|
||||
await login(providerId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`Unknown command: ${command}`);
|
||||
console.error(`Use 'npx @earendil-works/pi-ai --help' for usage`);
|
||||
process.exit(1);
|
||||
throw new Error(`Unknown command: ${command}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Error:", err.message);
|
||||
main().catch((error: unknown) => {
|
||||
console.error("Error:", error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ import { openAICodexResponsesApi } from "./api/openai-codex-responses.lazy.ts";
|
||||
import { openAICompletionsApi } from "./api/openai-completions.lazy.ts";
|
||||
import { openAIResponsesApi } from "./api/openai-responses.lazy.ts";
|
||||
import { getEnvApiKey } from "./env-api-keys.ts";
|
||||
import type { ModelsApiStreamOptions } from "./models.ts";
|
||||
import { builtinModels, getBuiltinModel, getBuiltinModels, getBuiltinProviders } from "./providers/all.ts";
|
||||
import { createFauxCore, type FauxProviderRegistration, type RegisterFauxProviderOptions } from "./providers/faux.ts";
|
||||
import type {
|
||||
@@ -221,9 +222,14 @@ function withEnvApiKey<TOptions extends StreamOptions>(
|
||||
return { ...options, apiKey } as TOptions;
|
||||
}
|
||||
|
||||
function shouldUseBuiltinModels(model: Model<Api>): boolean {
|
||||
const builtin = compatModels.getModel(model.provider, model.id);
|
||||
return builtin?.api === model.api && getApiProvider(model.api) === builtinApiProviderInstances.get(model.api);
|
||||
function hasResolvedCloudflareAuth(options: StreamOptions | undefined): boolean {
|
||||
return hasExplicitApiKey(options?.apiKey) || typeof options?.headers?.["cf-aig-authorization"] === "string";
|
||||
}
|
||||
|
||||
function getBuiltinProviderForModel(model: Model<Api>) {
|
||||
if (getApiProvider(model.api) !== builtinApiProviderInstances.get(model.api)) return undefined;
|
||||
const provider = compatModels.getProvider(model.provider);
|
||||
return provider?.getModels().some((candidate) => candidate.api === model.api) ? provider : undefined;
|
||||
}
|
||||
|
||||
function resolveApiProvider(api: Api) {
|
||||
@@ -239,8 +245,12 @@ export function stream<TApi extends Api>(
|
||||
context: Context,
|
||||
options?: ProviderStreamOptions,
|
||||
): AssistantMessageEventStream {
|
||||
if (shouldUseBuiltinModels(model)) {
|
||||
return compatModels.stream(model, context, options as ApiStreamOptions<TApi> | undefined);
|
||||
const builtinProvider = getBuiltinProviderForModel(model);
|
||||
if (builtinProvider) {
|
||||
if (model.provider.startsWith("cloudflare-") && !hasResolvedCloudflareAuth(options)) {
|
||||
return compatModels.stream(model, context, options as ModelsApiStreamOptions<TApi> | undefined);
|
||||
}
|
||||
return builtinProvider.stream(model, context, withEnvApiKey(model, options) as ApiStreamOptions<TApi>);
|
||||
}
|
||||
const provider = resolveApiProvider(model.api);
|
||||
return provider.stream(model, context, withEnvApiKey(model, options) as StreamOptions);
|
||||
@@ -260,8 +270,12 @@ export function streamSimple<TApi extends Api>(
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
): AssistantMessageEventStream {
|
||||
if (shouldUseBuiltinModels(model)) {
|
||||
return compatModels.streamSimple(model, context, options);
|
||||
const builtinProvider = getBuiltinProviderForModel(model);
|
||||
if (builtinProvider) {
|
||||
if (model.provider.startsWith("cloudflare-") && !hasResolvedCloudflareAuth(options)) {
|
||||
return compatModels.streamSimple(model, context, options);
|
||||
}
|
||||
return builtinProvider.streamSimple(model, context, withEnvApiKey(model, options));
|
||||
}
|
||||
const provider = resolveApiProvider(model.api);
|
||||
return provider.streamSimple(model, context, withEnvApiKey(model, options));
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { OAuthCredentials } from "../auth/types.ts";
|
||||
|
||||
/** Legacy extension OAuth prompt. */
|
||||
export interface OAuthPrompt {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
allowEmpty?: boolean;
|
||||
}
|
||||
|
||||
/** Legacy extension OAuth authorization link. */
|
||||
export interface OAuthAuthInfo {
|
||||
url: string;
|
||||
instructions?: string;
|
||||
}
|
||||
|
||||
/** Legacy extension OAuth device-code notification. */
|
||||
export interface OAuthDeviceCodeInfo {
|
||||
userCode: string;
|
||||
verificationUri: string;
|
||||
intervalSeconds?: number;
|
||||
expiresInSeconds?: number;
|
||||
}
|
||||
|
||||
export interface OAuthSelectOption {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface OAuthSelectPrompt {
|
||||
message: string;
|
||||
options: OAuthSelectOption[];
|
||||
}
|
||||
|
||||
/** Callback surface retained only for coding-agent extension compatibility. */
|
||||
export interface OAuthLoginCallbacks {
|
||||
onAuth(info: OAuthAuthInfo): void;
|
||||
onDeviceCode(info: OAuthDeviceCodeInfo): void;
|
||||
onPrompt(prompt: OAuthPrompt): Promise<string>;
|
||||
onProgress?(message: string): void;
|
||||
onManualCodeInput?(): Promise<string>;
|
||||
onSelect(prompt: OAuthSelectPrompt): Promise<string | undefined>;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export type { OAuthCredentials };
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defaultProviderAuthContext as defaultAuthContext } from "./auth/context.ts";
|
||||
import { InMemoryCredentialStore } from "./auth/credential-store.ts";
|
||||
import { ModelsError, resolveProviderAuth } from "./auth/resolve.ts";
|
||||
import { type AuthResolutionOverrides, ModelsError, resolveProviderAuth } from "./auth/resolve.ts";
|
||||
import type { AuthContext, AuthResult, CredentialStore, ProviderAuth } from "./auth/types.ts";
|
||||
import type { CreateModelsOptions } from "./models.ts";
|
||||
import type { AssistantImages, ImagesApi, ImagesContext, ImagesModel, ImagesOptions, ProviderImages } from "./types.ts";
|
||||
@@ -68,11 +68,12 @@ export interface ImagesModels {
|
||||
refresh(provider?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Resolve request auth for an image model. Same contract as
|
||||
* Resolve request auth by provider id or image model. Same contract as
|
||||
* `Models.getAuth()`: undefined when unknown/unconfigured, rejects with
|
||||
* `ModelsError` ("oauth"/"auth") on real failures.
|
||||
*/
|
||||
getAuth(model: ImagesModel<ImagesApi>): Promise<AuthResult | undefined>;
|
||||
getAuth(providerId: string, overrides?: AuthResolutionOverrides): Promise<AuthResult | undefined>;
|
||||
getAuth(model: ImagesModel<ImagesApi>, overrides?: AuthResolutionOverrides): Promise<AuthResult | undefined>;
|
||||
|
||||
/**
|
||||
* Generate images through the owning provider with auth resolved and
|
||||
@@ -167,10 +168,16 @@ class ImagesModelsImpl implements MutableImagesModels {
|
||||
await Promise.allSettled(Array.from(this.providers.values(), async (entry) => entry.refreshModels?.()));
|
||||
}
|
||||
|
||||
async getAuth(model: ImagesModel<ImagesApi>): Promise<AuthResult | undefined> {
|
||||
const provider = this.providers.get(model.provider);
|
||||
getAuth(providerId: string, overrides?: AuthResolutionOverrides): Promise<AuthResult | undefined>;
|
||||
getAuth(model: ImagesModel<ImagesApi>, overrides?: AuthResolutionOverrides): Promise<AuthResult | undefined>;
|
||||
async getAuth(
|
||||
providerOrModel: string | ImagesModel<ImagesApi>,
|
||||
overrides?: AuthResolutionOverrides,
|
||||
): Promise<AuthResult | undefined> {
|
||||
const providerId = typeof providerOrModel === "string" ? providerOrModel : providerOrModel.provider;
|
||||
const provider = this.providers.get(providerId);
|
||||
if (!provider) return undefined;
|
||||
return resolveProviderAuth(provider, model, this.credentials, this.authContext);
|
||||
return resolveProviderAuth(provider, this.credentials, this.authContext, overrides);
|
||||
}
|
||||
|
||||
async generateImages(
|
||||
@@ -184,7 +191,7 @@ class ImagesModelsImpl implements MutableImagesModels {
|
||||
throw new ModelsError("provider", `Unknown provider: ${model.provider}`);
|
||||
}
|
||||
|
||||
const resolution = await resolveProviderAuth(provider, model, this.credentials, this.authContext, {
|
||||
const resolution = await this.getAuth(model, {
|
||||
apiKey: options?.apiKey,
|
||||
env: options?.env,
|
||||
});
|
||||
|
||||
@@ -21,6 +21,14 @@ export * from "./auth/context.ts";
|
||||
export * from "./auth/credential-store.ts";
|
||||
export * from "./auth/helpers.ts";
|
||||
export * from "./auth/types.ts";
|
||||
export type {
|
||||
OAuthAuthInfo,
|
||||
OAuthDeviceCodeInfo,
|
||||
OAuthLoginCallbacks,
|
||||
OAuthPrompt,
|
||||
OAuthSelectOption,
|
||||
OAuthSelectPrompt,
|
||||
} from "./compat/extension-oauth-types.ts";
|
||||
export * from "./images-models.ts";
|
||||
export * from "./models.ts";
|
||||
export * from "./providers/faux.ts";
|
||||
@@ -29,19 +37,6 @@ export * from "./types.ts";
|
||||
export * from "./utils/diagnostics.ts";
|
||||
export * from "./utils/event-stream.ts";
|
||||
export * from "./utils/json-parse.ts";
|
||||
export type {
|
||||
OAuthAuthInfo,
|
||||
OAuthCredentials,
|
||||
OAuthDeviceCodeInfo,
|
||||
OAuthLoginCallbacks,
|
||||
OAuthPrompt,
|
||||
OAuthProvider,
|
||||
OAuthProviderId,
|
||||
OAuthProviderInfo,
|
||||
OAuthProviderInterface,
|
||||
OAuthSelectOption,
|
||||
OAuthSelectPrompt,
|
||||
} from "./utils/oauth/types.ts";
|
||||
export * from "./utils/overflow.ts";
|
||||
export * from "./utils/retry.ts";
|
||||
export * from "./utils/typebox-helpers.ts";
|
||||
|
||||
+192
-39
@@ -1,8 +1,17 @@
|
||||
import { lazyStream } from "./api/lazy.ts";
|
||||
import { defaultProviderAuthContext as defaultAuthContext } from "./auth/context.ts";
|
||||
import { InMemoryCredentialStore } from "./auth/credential-store.ts";
|
||||
import { ModelsError, resolveProviderAuth } from "./auth/resolve.ts";
|
||||
import type { AuthContext, AuthResult, CredentialStore, ProviderAuth } from "./auth/types.ts";
|
||||
import { type AuthResolutionOverrides, ModelsError, resolveProviderAuth } from "./auth/resolve.ts";
|
||||
import type {
|
||||
AuthCheck,
|
||||
AuthContext,
|
||||
AuthInteraction,
|
||||
AuthResult,
|
||||
AuthType,
|
||||
Credential,
|
||||
CredentialStore,
|
||||
ProviderAuth,
|
||||
} from "./auth/types.ts";
|
||||
import type {
|
||||
Api,
|
||||
ApiStreamOptions,
|
||||
@@ -19,7 +28,15 @@ import type {
|
||||
Usage,
|
||||
} from "./types.ts";
|
||||
|
||||
export { type AuthModel, ModelsError, type ModelsErrorCode } from "./auth/resolve.ts";
|
||||
export { ModelsError, type ModelsErrorCode } from "./auth/resolve.ts";
|
||||
|
||||
export interface ModelsStreamTransforms {
|
||||
/** Transform fully assembled model/auth/request headers before provider dispatch. */
|
||||
transformHeaders?: (headers: ProviderHeaders) => ProviderHeaders | Promise<ProviderHeaders>;
|
||||
}
|
||||
|
||||
export type ModelsApiStreamOptions<TApi extends Api> = ApiStreamOptions<TApi> & ModelsStreamTransforms;
|
||||
export type ModelsSimpleStreamOptions = SimpleStreamOptions & ModelsStreamTransforms;
|
||||
|
||||
/**
|
||||
* A provider is the concrete runtime unit. It owns id/name/base metadata,
|
||||
@@ -63,6 +80,13 @@ export interface Provider<TApi extends Api = Api> {
|
||||
*/
|
||||
refreshModels?(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Optional provider policy for credential-specific model availability.
|
||||
* `getModels()` remains the complete synchronous catalog; `Models.getAvailable()`
|
||||
* applies this filter after confirming that provider auth is configured.
|
||||
*/
|
||||
filterModels?(models: readonly Model<TApi>[], credential: Credential | undefined): readonly Model<TApi>[];
|
||||
|
||||
stream<T extends TApi>(
|
||||
model: Model<T>,
|
||||
context: Context,
|
||||
@@ -101,31 +125,44 @@ export interface Models {
|
||||
*/
|
||||
refresh(provider?: string): Promise<void>;
|
||||
|
||||
/** Check whether a provider has complete auth configuration without refreshing OAuth. */
|
||||
checkAuth(providerId: string): Promise<AuthCheck | undefined>;
|
||||
|
||||
/** Return models whose providers have complete auth configuration. */
|
||||
getAvailable(providerId?: string): Promise<readonly Model<Api>[]>;
|
||||
|
||||
/**
|
||||
* Resolve request auth for a model. Includes a source label for status UI.
|
||||
* Resolve provider-scoped auth by provider id, or provider auth plus static
|
||||
* model headers when passed a model. Includes a source label for status UI.
|
||||
* Resolves `undefined` when the provider is unknown or unconfigured.
|
||||
* Rejects with `ModelsError`: code "oauth" when a token refresh fails (the
|
||||
* stored credential is preserved for retry; re-login fixes it), code "auth"
|
||||
* when api-key resolution or the credential store fails. Request paths
|
||||
* surface rejections as stream errors; status/availability UIs catch them
|
||||
* and render "needs re-login" instead of treating them as unconfigured.
|
||||
* surface rejections as stream errors.
|
||||
*/
|
||||
getAuth(model: Model<Api>): Promise<AuthResult | undefined>;
|
||||
getAuth(providerId: string, overrides?: AuthResolutionOverrides): Promise<AuthResult | undefined>;
|
||||
getAuth(model: Model<Api>, overrides?: AuthResolutionOverrides): Promise<AuthResult | undefined>;
|
||||
|
||||
/** Run a provider-owned login flow and persist its returned credential. */
|
||||
login(providerId: string, type: AuthType, interaction: AuthInteraction): Promise<Credential>;
|
||||
|
||||
/** Remove the stored credential for a provider. */
|
||||
logout(providerId: string): Promise<void>;
|
||||
|
||||
stream<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: ApiStreamOptions<TApi>,
|
||||
options?: ModelsApiStreamOptions<TApi>,
|
||||
): AssistantMessageEventStream;
|
||||
|
||||
complete<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: ApiStreamOptions<TApi>,
|
||||
options?: ModelsApiStreamOptions<TApi>,
|
||||
): Promise<AssistantMessage>;
|
||||
|
||||
streamSimple(model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream;
|
||||
completeSimple(model: Model<Api>, context: Context, options?: SimpleStreamOptions): Promise<AssistantMessage>;
|
||||
streamSimple(model: Model<Api>, context: Context, options?: ModelsSimpleStreamOptions): AssistantMessageEventStream;
|
||||
completeSimple(model: Model<Api>, context: Context, options?: ModelsSimpleStreamOptions): Promise<AssistantMessage>;
|
||||
}
|
||||
|
||||
export interface MutableModels extends Models {
|
||||
@@ -140,6 +177,22 @@ export interface CreateModelsOptions {
|
||||
authContext?: AuthContext;
|
||||
}
|
||||
|
||||
function mergeHeaders(
|
||||
base: ProviderHeaders | undefined,
|
||||
override: ProviderHeaders | undefined,
|
||||
): ProviderHeaders | undefined {
|
||||
if (!base && !override) return undefined;
|
||||
const merged = { ...base };
|
||||
for (const [name, value] of Object.entries(override ?? {})) {
|
||||
const lowerName = name.toLowerCase();
|
||||
for (const existingName of Object.keys(merged)) {
|
||||
if (existingName.toLowerCase() === lowerName) delete merged[existingName];
|
||||
}
|
||||
merged[name] = value;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
class ModelsImpl implements MutableModels {
|
||||
private providers = new Map<string, Provider>();
|
||||
private credentials: CredentialStore;
|
||||
@@ -214,10 +267,103 @@ class ModelsImpl implements MutableModels {
|
||||
await Promise.allSettled(Array.from(this.providers.values(), async (entry) => entry.refreshModels?.()));
|
||||
}
|
||||
|
||||
async getAuth(model: Model<Api>): Promise<AuthResult | undefined> {
|
||||
const provider = this.providers.get(model.provider);
|
||||
private async readCredential(providerId: string): Promise<Credential | undefined> {
|
||||
try {
|
||||
return await this.credentials.read(providerId);
|
||||
} catch (error) {
|
||||
throw new ModelsError("auth", `Credential store read failed for ${providerId}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
private async checkProviderAuth(
|
||||
provider: Provider,
|
||||
credential: Credential | undefined,
|
||||
): Promise<AuthCheck | undefined> {
|
||||
if (credential?.type === "oauth") {
|
||||
return provider.auth.oauth ? { source: "OAuth", type: "oauth" } : undefined;
|
||||
}
|
||||
const apiKey = provider.auth.apiKey;
|
||||
if (!apiKey) return undefined;
|
||||
if (apiKey.check) {
|
||||
try {
|
||||
return await apiKey.check({
|
||||
ctx: this.authContext,
|
||||
credential: credential?.type === "api_key" ? credential : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new ModelsError("auth", `API key auth check failed for provider ${provider.id}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
const resolution = await resolveProviderAuth(provider, this.credentials, this.authContext);
|
||||
return resolution ? { source: resolution.source, type: "api_key" } : undefined;
|
||||
}
|
||||
|
||||
async checkAuth(providerId: string): Promise<AuthCheck | undefined> {
|
||||
const provider = this.providers.get(providerId);
|
||||
if (!provider) return undefined;
|
||||
return resolveProviderAuth(provider, model, this.credentials, this.authContext);
|
||||
return this.checkProviderAuth(provider, await this.readCredential(providerId));
|
||||
}
|
||||
|
||||
async getAvailable(providerId?: string): Promise<readonly Model<Api>[]> {
|
||||
const providers = providerId
|
||||
? [this.providers.get(providerId)].filter((entry) => entry !== undefined)
|
||||
: this.getProviders();
|
||||
const checks = await Promise.all(
|
||||
providers.map(async (provider) => {
|
||||
const credential = await this.readCredential(provider.id);
|
||||
return { provider, credential, auth: await this.checkProviderAuth(provider, credential) };
|
||||
}),
|
||||
);
|
||||
return checks.flatMap(({ provider, credential, auth }) => {
|
||||
if (!auth) return [];
|
||||
const models = provider.getModels();
|
||||
return provider.filterModels?.(models, credential) ?? models;
|
||||
});
|
||||
}
|
||||
|
||||
getAuth(providerId: string, overrides?: AuthResolutionOverrides): Promise<AuthResult | undefined>;
|
||||
getAuth(model: Model<Api>, overrides?: AuthResolutionOverrides): Promise<AuthResult | undefined>;
|
||||
async getAuth(
|
||||
providerOrModel: string | Model<Api>,
|
||||
overrides?: AuthResolutionOverrides,
|
||||
): Promise<AuthResult | undefined> {
|
||||
const providerId = typeof providerOrModel === "string" ? providerOrModel : providerOrModel.provider;
|
||||
const provider = this.providers.get(providerId);
|
||||
if (!provider) return undefined;
|
||||
const result = await resolveProviderAuth(provider, this.credentials, this.authContext, overrides);
|
||||
if (!result || typeof providerOrModel === "string" || !providerOrModel.headers) return result;
|
||||
return {
|
||||
...result,
|
||||
auth: {
|
||||
...result.auth,
|
||||
headers: mergeHeaders(result.auth.headers, providerOrModel.headers),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async login(providerId: string, type: AuthType, interaction: AuthInteraction): Promise<Credential> {
|
||||
const provider = this.providers.get(providerId);
|
||||
if (!provider) throw new ModelsError("provider", `Unknown provider: ${providerId}`);
|
||||
const method = type === "oauth" ? provider.auth.oauth : provider.auth.apiKey;
|
||||
if (!method?.login) {
|
||||
throw new ModelsError("auth", `${provider.name} does not support ${type} login`);
|
||||
}
|
||||
const credential = await method.login(interaction);
|
||||
try {
|
||||
await this.credentials.modify(providerId, async () => credential);
|
||||
} catch (error) {
|
||||
throw new ModelsError("auth", `Credential store modify failed for ${providerId}`, { cause: error });
|
||||
}
|
||||
return credential;
|
||||
}
|
||||
|
||||
async logout(providerId: string): Promise<void> {
|
||||
try {
|
||||
await this.credentials.delete(providerId);
|
||||
} catch (error) {
|
||||
throw new ModelsError("auth", `Credential store delete failed for ${providerId}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
private requireProvider(model: Model<Api>): Provider {
|
||||
@@ -228,30 +374,28 @@ class ModelsImpl implements MutableModels {
|
||||
return provider;
|
||||
}
|
||||
|
||||
private async applyAuth<TOptions extends StreamOptions>(
|
||||
private async applyAuth<TOptions extends StreamOptions & ModelsStreamTransforms>(
|
||||
model: Model<Api>,
|
||||
options: TOptions | undefined,
|
||||
): Promise<{ requestModel: Model<Api>; requestOptions: TOptions | undefined }> {
|
||||
const resolution = await resolveProviderAuth(
|
||||
this.requireProvider(model),
|
||||
model,
|
||||
this.credentials,
|
||||
this.authContext,
|
||||
{
|
||||
apiKey: options?.apiKey,
|
||||
env: options?.env,
|
||||
},
|
||||
);
|
||||
const auth = resolution?.auth;
|
||||
if (!auth) return { requestModel: model, requestOptions: options };
|
||||
): Promise<{ requestModel: Model<Api>; requestOptions: StreamOptions | undefined }> {
|
||||
this.requireProvider(model);
|
||||
const resolution = await this.getAuth(model, {
|
||||
apiKey: options?.apiKey,
|
||||
env: options?.env,
|
||||
});
|
||||
if (!resolution) {
|
||||
throw new ModelsError("auth", `Provider is not configured: ${model.provider}`);
|
||||
}
|
||||
const auth = resolution.auth;
|
||||
|
||||
const requestModel = auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model;
|
||||
|
||||
// Explicit request options win per-field; headers/env merge per key.
|
||||
// Explicit request options win per-field; the Models-only transform runs last.
|
||||
const apiKey = options?.apiKey ?? auth.apiKey;
|
||||
const headers = auth.headers || options?.headers ? { ...auth.headers, ...options?.headers } : undefined;
|
||||
let headers = mergeHeaders(auth.headers, options?.headers);
|
||||
if (options?.transformHeaders) headers = await options.transformHeaders(headers ?? {});
|
||||
const env = resolution.env || options?.env ? { ...(resolution.env ?? {}), ...(options?.env ?? {}) } : undefined;
|
||||
const requestOptions = { ...options, apiKey, headers, env } as TOptions;
|
||||
const requestModel = auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model;
|
||||
const { transformHeaders: _transformHeaders, ...providerOptions } = options ?? {};
|
||||
const requestOptions = { ...providerOptions, apiKey, headers, env } as StreamOptions;
|
||||
|
||||
return { requestModel, requestOptions };
|
||||
}
|
||||
@@ -259,11 +403,14 @@ class ModelsImpl implements MutableModels {
|
||||
stream<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: ApiStreamOptions<TApi>,
|
||||
options?: ModelsApiStreamOptions<TApi>,
|
||||
): AssistantMessageEventStream {
|
||||
return lazyStream(model, async () => {
|
||||
const provider = this.requireProvider(model);
|
||||
const { requestModel, requestOptions } = await this.applyAuth(model, options as StreamOptions | undefined);
|
||||
const { requestModel, requestOptions } = await this.applyAuth(
|
||||
model,
|
||||
options as ModelsApiStreamOptions<Api> | undefined,
|
||||
);
|
||||
return provider.stream(requestModel as Model<TApi>, context, requestOptions as ApiStreamOptions<TApi>);
|
||||
});
|
||||
}
|
||||
@@ -271,20 +418,24 @@ class ModelsImpl implements MutableModels {
|
||||
async complete<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: ApiStreamOptions<TApi>,
|
||||
options?: ModelsApiStreamOptions<TApi>,
|
||||
): Promise<AssistantMessage> {
|
||||
return this.stream(model, context, options).result();
|
||||
}
|
||||
|
||||
streamSimple(model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
|
||||
streamSimple(model: Model<Api>, context: Context, options?: ModelsSimpleStreamOptions): AssistantMessageEventStream {
|
||||
return lazyStream(model, async () => {
|
||||
const provider = this.requireProvider(model);
|
||||
const { requestModel, requestOptions } = await this.applyAuth(model, options);
|
||||
return provider.streamSimple(requestModel, context, requestOptions);
|
||||
return provider.streamSimple(requestModel, context, requestOptions as SimpleStreamOptions);
|
||||
});
|
||||
}
|
||||
|
||||
async completeSimple(model: Model<Api>, context: Context, options?: SimpleStreamOptions): Promise<AssistantMessage> {
|
||||
async completeSimple(
|
||||
model: Model<Api>,
|
||||
context: Context,
|
||||
options?: ModelsSimpleStreamOptions,
|
||||
): Promise<AssistantMessage> {
|
||||
return this.streamSimple(model, context, options).result();
|
||||
}
|
||||
}
|
||||
@@ -311,6 +462,7 @@ export interface CreateProviderOptions<TApi extends Api = Api> {
|
||||
* `Models.refresh(provider)`), and a later call retries.
|
||||
*/
|
||||
refreshModels?: () => Promise<readonly Model<TApi>[]>;
|
||||
filterModels?: (models: readonly Model<TApi>[], credential: Credential | undefined) => readonly Model<TApi>[];
|
||||
/** Single implementation, or map keyed by `model.api` for mixed-API providers. */
|
||||
api: ProviderStreams | Partial<Record<TApi, ProviderStreams>>;
|
||||
}
|
||||
@@ -363,6 +515,7 @@ export function createProvider<TApi extends Api = Api>(input: CreateProviderOpti
|
||||
return inflightRefresh;
|
||||
}
|
||||
: undefined,
|
||||
filterModels: input.filterModels,
|
||||
stream: (model, context, options) => dispatch(model, (streams) => streams.stream(model, context, options)),
|
||||
streamSimple: (model, context, options) =>
|
||||
dispatch(model, (streams) => streams.streamSimple(model, context, options)),
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
export * from "./utils/oauth/index.ts";
|
||||
/** Type-only compatibility entry point for coding-agent extension OAuth declarations. */
|
||||
export type {
|
||||
OAuthAuthInfo,
|
||||
OAuthCredentials,
|
||||
OAuthDeviceCodeInfo,
|
||||
OAuthLoginCallbacks,
|
||||
OAuthPrompt,
|
||||
OAuthSelectOption,
|
||||
OAuthSelectPrompt,
|
||||
} from "./compat/extension-oauth-types.ts";
|
||||
|
||||
@@ -1290,6 +1290,60 @@ export const AMAZON_BEDROCK_MODELS = {
|
||||
contextWindow: 272000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"bedrock-converse-stream">,
|
||||
"openai.gpt-5.6-luna": {
|
||||
id: "openai.gpt-5.6-luna",
|
||||
name: "GPT-5.6 Luna",
|
||||
api: "bedrock-converse-stream",
|
||||
provider: "amazon-bedrock",
|
||||
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"xhigh"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 6,
|
||||
cacheRead: 0.1,
|
||||
cacheWrite: 1.25,
|
||||
},
|
||||
contextWindow: 272000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"bedrock-converse-stream">,
|
||||
"openai.gpt-5.6-sol": {
|
||||
id: "openai.gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
api: "bedrock-converse-stream",
|
||||
provider: "amazon-bedrock",
|
||||
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"xhigh"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 5,
|
||||
output: 30,
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 6.25,
|
||||
},
|
||||
contextWindow: 272000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"bedrock-converse-stream">,
|
||||
"openai.gpt-5.6-terra": {
|
||||
id: "openai.gpt-5.6-terra",
|
||||
name: "GPT-5.6 Terra",
|
||||
api: "bedrock-converse-stream",
|
||||
provider: "amazon-bedrock",
|
||||
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"xhigh"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 2.5,
|
||||
output: 15,
|
||||
cacheRead: 0.25,
|
||||
cacheWrite: 3.125,
|
||||
},
|
||||
contextWindow: 272000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"bedrock-converse-stream">,
|
||||
"openai.gpt-oss-120b": {
|
||||
id: "openai.gpt-oss-120b",
|
||||
name: "gpt-oss-120b",
|
||||
|
||||
@@ -4,16 +4,61 @@ import { createProvider, type Provider } from "../models.ts";
|
||||
import { AMAZON_BEDROCK_MODELS } from "./amazon-bedrock.models.ts";
|
||||
|
||||
/**
|
||||
* Bedrock auth is ambient: the AWS SDK's default credential chain handles the
|
||||
* actual signing, so `resolve` only reports whether the provider is
|
||||
* configured. A stored credential key is surfaced as the bearer token.
|
||||
* 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",
|
||||
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 }, source: "stored credential" };
|
||||
if (await ctx.env("AWS_BEARER_TOKEN_BEDROCK")) return { auth: {}, source: "AWS_BEARER_TOKEN_BEDROCK" };
|
||||
if (await ctx.env("AWS_PROFILE")) return { auth: {}, source: "AWS_PROFILE" };
|
||||
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" };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
|
||||
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
|
||||
import { loadAnthropicOAuth } from "../auth/oauth/load.ts";
|
||||
import { createProvider, type Provider } from "../models.ts";
|
||||
import { loadAnthropicOAuth } from "../utils/oauth/load.ts";
|
||||
import { ANTHROPIC_MODELS } from "./anthropic.models.ts";
|
||||
|
||||
export function anthropicProvider(): Provider<"anthropic-messages"> {
|
||||
|
||||
@@ -660,6 +660,23 @@ export const AZURE_OPENAI_RESPONSES_MODELS = {
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"azure-openai-responses">,
|
||||
"gpt-realtime-2.1": {
|
||||
id: "gpt-realtime-2.1",
|
||||
name: "GPT-Realtime-2.1",
|
||||
api: "azure-openai-responses",
|
||||
provider: "azure-openai-responses",
|
||||
baseUrl: "",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 4,
|
||||
output: 24,
|
||||
cacheRead: 0.4,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 32000,
|
||||
} satisfies Model<"azure-openai-responses">,
|
||||
"o1": {
|
||||
id: "o1",
|
||||
name: "o1",
|
||||
|
||||
@@ -52,7 +52,7 @@ export const CEREBRAS_MODELS = {
|
||||
cost: {
|
||||
input: 2.25,
|
||||
output: 2.75,
|
||||
cacheRead: 0,
|
||||
cacheRead: 2.25,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
|
||||
@@ -528,6 +528,60 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = {
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"gpt-5.6-luna": {
|
||||
id: "gpt-5.6-luna",
|
||||
name: "GPT-5.6 Luna",
|
||||
api: "openai-responses",
|
||||
provider: "cloudflare-ai-gateway",
|
||||
baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 6,
|
||||
cacheRead: 0.1,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"gpt-5.6-sol": {
|
||||
id: "gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
api: "openai-responses",
|
||||
provider: "cloudflare-ai-gateway",
|
||||
baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 5,
|
||||
output: 30,
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"gpt-5.6-terra": {
|
||||
id: "gpt-5.6-terra",
|
||||
name: "GPT-5.6 Terra",
|
||||
api: "openai-responses",
|
||||
provider: "cloudflare-ai-gateway",
|
||||
baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 2.5,
|
||||
output: 15,
|
||||
cacheRead: 0.25,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"o1": {
|
||||
id: "o1",
|
||||
name: "o1",
|
||||
@@ -685,4 +739,22 @@ export const CLOUDFLARE_AI_GATEWAY_MODELS = {
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"workers-ai/@cf/zai-org/glm-5.2": {
|
||||
id: "workers-ai/@cf/zai-org/glm-5.2",
|
||||
name: "Glm 5.2",
|
||||
api: "openai-completions",
|
||||
provider: "cloudflare-ai-gateway",
|
||||
baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat",
|
||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"sendSessionAffinityHeaders":true},
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1.4,
|
||||
output: 4.4,
|
||||
cacheRead: 0.26,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
} as const;
|
||||
|
||||
@@ -4,6 +4,7 @@ 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"
|
||||
@@ -14,9 +15,9 @@ export function cloudflareAIGatewayProvider(): Provider<
|
||||
auth: { apiKey: cloudflareAIGatewayAuth() },
|
||||
models: Object.values(CLOUDFLARE_AI_GATEWAY_MODELS),
|
||||
api: {
|
||||
"anthropic-messages": anthropicMessagesApi(),
|
||||
"openai-completions": openAICompletionsApi(),
|
||||
"openai-responses": openAIResponsesApi(),
|
||||
"anthropic-messages": cloudflareStreams(anthropicMessagesApi()),
|
||||
"openai-completions": cloudflareStreams(openAICompletionsApi()),
|
||||
"openai-responses": cloudflareStreams(openAIResponsesApi()),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ApiKeyAuth, ApiKeyCredential, AuthContext } from "../auth/types.ts";
|
||||
import type { Api, ImagesApi, ImagesModel, Model, ProviderEnv } from "../types.ts";
|
||||
import type { ProviderEnv } from "../types.ts";
|
||||
|
||||
const CLOUDFLARE_API_KEY = "CLOUDFLARE_API_KEY";
|
||||
const CLOUDFLARE_ACCOUNT_ID = "CLOUDFLARE_ACCOUNT_ID";
|
||||
@@ -19,22 +19,11 @@ async function resolveValue(
|
||||
return ctx.env(name);
|
||||
}
|
||||
|
||||
function resolveCloudflareBaseUrl(
|
||||
model: Model<Api> | ImagesModel<ImagesApi>,
|
||||
accountId: string,
|
||||
gatewayId: string | undefined,
|
||||
): string {
|
||||
return model.baseUrl
|
||||
.replaceAll(`{${CLOUDFLARE_ACCOUNT_ID}}`, accountId)
|
||||
.replaceAll(`{${CLOUDFLARE_GATEWAY_ID}}`, gatewayId ?? "");
|
||||
}
|
||||
|
||||
async function resolveCloudflareEnv(
|
||||
kind: CloudflareAuthKind,
|
||||
model: Model<Api> | ImagesModel<ImagesApi>,
|
||||
ctx: AuthContext,
|
||||
credential: ApiKeyCredential | undefined,
|
||||
): Promise<{ apiKey: string; env: ProviderEnv; baseUrl: string; source: string } | 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;
|
||||
@@ -47,7 +36,6 @@ async function resolveCloudflareEnv(
|
||||
CLOUDFLARE_ACCOUNT_ID: accountId,
|
||||
...(gatewayId ? { CLOUDFLARE_GATEWAY_ID: gatewayId } : {}),
|
||||
},
|
||||
baseUrl: resolveCloudflareBaseUrl(model, accountId, gatewayId),
|
||||
source: credential ? "stored credential" : CLOUDFLARE_API_KEY,
|
||||
};
|
||||
}
|
||||
@@ -55,16 +43,16 @@ async function resolveCloudflareEnv(
|
||||
export function cloudflareWorkersAIAuth(): ApiKeyAuth {
|
||||
return {
|
||||
name: "Cloudflare API key",
|
||||
login: async (callbacks) => {
|
||||
const key = await callbacks.prompt({ type: "secret", message: "Enter Cloudflare API key" });
|
||||
const accountId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare account ID" });
|
||||
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 ({ model, ctx, credential }) => {
|
||||
const resolved = await resolveCloudflareEnv("workers-ai", model, ctx, credential);
|
||||
resolve: async ({ ctx, credential }) => {
|
||||
const resolved = await resolveCloudflareEnv("workers-ai", ctx, credential);
|
||||
if (!resolved) return undefined;
|
||||
return {
|
||||
auth: { apiKey: resolved.apiKey, baseUrl: resolved.baseUrl },
|
||||
auth: { apiKey: resolved.apiKey },
|
||||
env: resolved.env,
|
||||
source: resolved.source,
|
||||
};
|
||||
@@ -75,18 +63,18 @@ export function cloudflareWorkersAIAuth(): ApiKeyAuth {
|
||||
export function cloudflareAIGatewayAuth(): ApiKeyAuth {
|
||||
return {
|
||||
name: "Cloudflare API key",
|
||||
login: async (callbacks) => {
|
||||
const key = await callbacks.prompt({ type: "secret", message: "Enter Cloudflare API key" });
|
||||
const accountId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare account ID" });
|
||||
const gatewayId = await callbacks.prompt({ type: "text", message: "Enter Cloudflare AI Gateway ID" });
|
||||
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 ({ model, ctx, credential }) => {
|
||||
const resolved = await resolveCloudflareEnv("ai-gateway", model, ctx, credential);
|
||||
resolve: async ({ ctx, credential }) => {
|
||||
const resolved = await resolveCloudflareEnv("ai-gateway", ctx, credential);
|
||||
if (!resolved) return undefined;
|
||||
return {
|
||||
auth: {
|
||||
@@ -95,7 +83,6 @@ export function cloudflareAIGatewayAuth(): ApiKeyAuth {
|
||||
Authorization: null,
|
||||
"x-api-key": null,
|
||||
},
|
||||
baseUrl: resolved.baseUrl,
|
||||
},
|
||||
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),
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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"> {
|
||||
@@ -9,6 +10,6 @@ export function cloudflareWorkersAIProvider(): Provider<"openai-completions"> {
|
||||
name: "Cloudflare Workers AI",
|
||||
auth: { apiKey: cloudflareWorkersAIAuth() },
|
||||
models: Object.values(CLOUDFLARE_WORKERS_AI_MODELS),
|
||||
api: openAICompletionsApi(),
|
||||
api: cloudflareStreams(openAICompletionsApi()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ export const GITHUB_COPILOT_MODELS = {
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 64000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"gemini-3.5-flash": {
|
||||
@@ -445,6 +445,63 @@ export const GITHUB_COPILOT_MODELS = {
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"gpt-5.6-luna": {
|
||||
id: "gpt-5.6-luna",
|
||||
name: "GPT-5.6 Luna",
|
||||
api: "openai-responses",
|
||||
provider: "github-copilot",
|
||||
baseUrl: "https://api.individual.githubcopilot.com",
|
||||
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh","max":"max"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 6,
|
||||
cacheRead: 0.1,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"gpt-5.6-sol": {
|
||||
id: "gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
api: "openai-responses",
|
||||
provider: "github-copilot",
|
||||
baseUrl: "https://api.individual.githubcopilot.com",
|
||||
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh","max":"max"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 5,
|
||||
output: 30,
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"gpt-5.6-terra": {
|
||||
id: "gpt-5.6-terra",
|
||||
name: "GPT-5.6 Terra",
|
||||
api: "openai-responses",
|
||||
provider: "github-copilot",
|
||||
baseUrl: "https://api.individual.githubcopilot.com",
|
||||
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh","max":"max"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 2.5,
|
||||
output: 15,
|
||||
cacheRead: 0.25,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"kimi-k2.7-code": {
|
||||
id: "kimi-k2.7-code",
|
||||
name: "Kimi K2.7 Code",
|
||||
|
||||
@@ -2,8 +2,8 @@ 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 { loadGitHubCopilotOAuth } from "../utils/oauth/load.ts";
|
||||
import { GITHUB_COPILOT_MODELS } from "./github-copilot.models.ts";
|
||||
|
||||
export function githubCopilotProvider(): Provider<"anthropic-messages" | "openai-completions" | "openai-responses"> {
|
||||
@@ -16,6 +16,15 @@ export function githubCopilotProvider(): Provider<"anthropic-messages" | "openai
|
||||
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(),
|
||||
|
||||
@@ -12,16 +12,71 @@ const VERTEX_ADC_PATH = "~/.config/gcloud/application_default_credentials.json";
|
||||
*/
|
||||
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 = await ctx.env("GOOGLE_APPLICATION_CREDENTIALS");
|
||||
const adcPath =
|
||||
credential?.env?.GOOGLE_APPLICATION_CREDENTIALS ?? (await ctx.env("GOOGLE_APPLICATION_CREDENTIALS"));
|
||||
const hasCredentials = await ctx.fileExists(adcPath ?? VERTEX_ADC_PATH);
|
||||
const hasProject = Boolean((await ctx.env("GOOGLE_CLOUD_PROJECT")) ?? (await ctx.env("GCLOUD_PROJECT")));
|
||||
const hasLocation = Boolean(await ctx.env("GOOGLE_CLOUD_LOCATION"));
|
||||
if (hasCredentials && hasProject && hasLocation) {
|
||||
return { auth: {}, source: "gcloud application default credentials" };
|
||||
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;
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { loadOpenAICodexOAuth } from "../utils/oauth/load.ts";
|
||||
import { OPENAI_CODEX_MODELS } from "./openai-codex.models.ts";
|
||||
|
||||
export function openaiCodexProvider(): Provider<"openai-codex-responses"> {
|
||||
|
||||
@@ -667,6 +667,23 @@ export const OPENAI_MODELS = {
|
||||
contextWindow: 272000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"gpt-realtime-2.1": {
|
||||
id: "gpt-realtime-2.1",
|
||||
name: "GPT-Realtime-2.1",
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 4,
|
||||
output: 24,
|
||||
cacheRead: 0.4,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 32000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"o1": {
|
||||
id: "o1",
|
||||
name: "o1",
|
||||
|
||||
@@ -674,6 +674,60 @@ export const OPENCODE_MODELS = {
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"gpt-5.6-luna": {
|
||||
id: "gpt-5.6-luna",
|
||||
name: "GPT-5.6 Luna",
|
||||
api: "openai-responses",
|
||||
provider: "opencode",
|
||||
baseUrl: "https://opencode.ai/zen/v1",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 6,
|
||||
cacheRead: 0.1,
|
||||
cacheWrite: 1.25,
|
||||
},
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"gpt-5.6-sol": {
|
||||
id: "gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
api: "openai-responses",
|
||||
provider: "opencode",
|
||||
baseUrl: "https://opencode.ai/zen/v1",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 5,
|
||||
output: 30,
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 6.25,
|
||||
},
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"gpt-5.6-terra": {
|
||||
id: "gpt-5.6-terra",
|
||||
name: "GPT-5.6 Terra",
|
||||
api: "openai-responses",
|
||||
provider: "opencode",
|
||||
baseUrl: "https://opencode.ai/zen/v1",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 2.5,
|
||||
output: 15,
|
||||
cacheRead: 0.25,
|
||||
cacheWrite: 3.125,
|
||||
},
|
||||
contextWindow: 1050000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-responses">,
|
||||
"grok-4.5": {
|
||||
id: "grok-4.5",
|
||||
name: "Grok 4.5",
|
||||
@@ -726,7 +780,7 @@ export const OPENCODE_MODELS = {
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
contextWindow: 190000,
|
||||
maxTokens: 64000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"kimi-k2.5": {
|
||||
|
||||
@@ -461,24 +461,6 @@ export const OPENROUTER_MODELS = {
|
||||
contextWindow: 262144,
|
||||
maxTokens: 80000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"arcee-ai/trinity-mini": {
|
||||
id: "arcee-ai/trinity-mini",
|
||||
name: "Arcee AI: Trinity Mini",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"},
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.045,
|
||||
output: 0.15,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"arcee-ai/virtuoso-large": {
|
||||
id: "arcee-ai/virtuoso-large",
|
||||
name: "Arcee AI: Virtuoso Large",
|
||||
@@ -687,8 +669,8 @@ export const OPENROUTER_MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.21,
|
||||
output: 0.79,
|
||||
input: 0.25,
|
||||
output: 0.95,
|
||||
cacheRead: 0.13,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -759,9 +741,9 @@ export const OPENROUTER_MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.2288,
|
||||
output: 0.3432,
|
||||
cacheRead: 0.02288,
|
||||
input: 0.2145,
|
||||
output: 0.32175,
|
||||
cacheRead: 0.02145,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
@@ -1121,13 +1103,13 @@ export const OPENROUTER_MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.12,
|
||||
input: 0.06,
|
||||
output: 0.35,
|
||||
cacheRead: 0.09,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"google/gemma-4-31b-it:free": {
|
||||
id: "google/gemma-4-31b-it:free",
|
||||
@@ -1145,7 +1127,7 @@ export const OPENROUTER_MODELS = {
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
maxTokens: 32768,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"ibm-granite/granite-4.1-8b": {
|
||||
id: "ibm-granite/granite-4.1-8b",
|
||||
@@ -1238,6 +1220,24 @@ export const OPENROUTER_MODELS = {
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"kwaipilot/kat-coder-air-v2.5": {
|
||||
id: "kwaipilot/kat-coder-air-v2.5",
|
||||
name: "Kwaipilot: KAT-Coder-Air V2.5",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"},
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.15,
|
||||
output: 0.6,
|
||||
cacheRead: 0.03,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 80000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"kwaipilot/kat-coder-pro-v2": {
|
||||
id: "kwaipilot/kat-coder-pro-v2",
|
||||
name: "Kwaipilot: KAT-Coder-Pro V2",
|
||||
@@ -1256,23 +1256,23 @@ export const OPENROUTER_MODELS = {
|
||||
contextWindow: 256000,
|
||||
maxTokens: 80000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"liquid/lfm-2.5-1.2b-thinking:free": {
|
||||
id: "liquid/lfm-2.5-1.2b-thinking:free",
|
||||
name: "LiquidAI: LFM2.5-1.2B-Thinking (free)",
|
||||
"kwaipilot/kat-coder-pro-v2.5": {
|
||||
id: "kwaipilot/kat-coder-pro-v2.5",
|
||||
name: "Kwaipilot: KAT-Coder-Pro V2.5",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"},
|
||||
reasoning: true,
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
input: 0.74,
|
||||
output: 2.96,
|
||||
cacheRead: 0.15,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 32768,
|
||||
maxTokens: 4096,
|
||||
contextWindow: 256000,
|
||||
maxTokens: 80000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3.1-70b-instruct": {
|
||||
id: "meta-llama/llama-3.1-70b-instruct",
|
||||
@@ -1356,8 +1356,8 @@ export const OPENROUTER_MODELS = {
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.15,
|
||||
output: 0.6,
|
||||
input: 0.2,
|
||||
output: 0.8,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -1878,9 +1878,9 @@ export const OPENROUTER_MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.65,
|
||||
input: 0.66,
|
||||
output: 3.41,
|
||||
cacheRead: 0.14,
|
||||
cacheRead: 0.15,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
@@ -1896,9 +1896,9 @@ export const OPENROUTER_MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.72,
|
||||
input: 0.719,
|
||||
output: 3.49,
|
||||
cacheRead: 0.159,
|
||||
cacheRead: 0.149,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
@@ -2456,11 +2456,11 @@ export const OPENROUTER_MODELS = {
|
||||
cost: {
|
||||
input: 0.05,
|
||||
output: 0.4,
|
||||
cacheRead: 0.01,
|
||||
cacheRead: 0.005,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
maxTokens: 4096,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-5-pro": {
|
||||
id: "openai/gpt-5-pro",
|
||||
@@ -2492,7 +2492,7 @@ export const OPENROUTER_MODELS = {
|
||||
cost: {
|
||||
input: 1.25,
|
||||
output: 10,
|
||||
cacheRead: 0.13,
|
||||
cacheRead: 0.125,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
@@ -2976,26 +2976,8 @@ export const OPENROUTER_MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.036,
|
||||
output: 0.18,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-oss-120b:free": {
|
||||
id: "openai/gpt-oss-120b:free",
|
||||
name: "OpenAI: gpt-oss-120b (free)",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
compat: {"thinkingFormat":"openrouter"},
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
input: 0.03,
|
||||
output: 0.15,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -3481,7 +3463,7 @@ export const OPENROUTER_MODELS = {
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.09,
|
||||
output: 0.1,
|
||||
output: 0.55,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -4074,13 +4056,13 @@ export const OPENROUTER_MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.285,
|
||||
input: 0.289,
|
||||
output: 2.4,
|
||||
cacheRead: 0.15,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262140,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3.6-35b-a3b": {
|
||||
id: "qwen/qwen3.6-35b-a3b",
|
||||
@@ -4561,12 +4543,12 @@ export const OPENROUTER_MODELS = {
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.43,
|
||||
output: 1.74,
|
||||
output: 1.75,
|
||||
cacheRead: 0.08,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 202752,
|
||||
maxTokens: 131072,
|
||||
contextWindow: 200000,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"z-ai/glm-4.6v": {
|
||||
id: "z-ai/glm-4.6v",
|
||||
@@ -4638,7 +4620,7 @@ export const OPENROUTER_MODELS = {
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 202752,
|
||||
maxTokens: 4096,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"z-ai/glm-5-turbo": {
|
||||
id: "z-ai/glm-5-turbo",
|
||||
@@ -4687,13 +4669,13 @@ export const OPENROUTER_MODELS = {
|
||||
thinkingLevelMap: {"xhigh":"xhigh"},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.54,
|
||||
output: 1.76,
|
||||
cacheRead: 0.1,
|
||||
input: 0.924,
|
||||
output: 2.904,
|
||||
cacheRead: 0.1716,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 101376,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"z-ai/glm-5v-turbo": {
|
||||
id: "z-ai/glm-5v-turbo",
|
||||
@@ -4831,9 +4813,9 @@ export const OPENROUTER_MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.65,
|
||||
input: 0.66,
|
||||
output: 3.41,
|
||||
cacheRead: 0.14,
|
||||
cacheRead: 0.15,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
|
||||
@@ -497,23 +497,6 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
contextWindow: 200000,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"anthropic/claude-3.5-haiku": {
|
||||
id: "anthropic/claude-3.5-haiku",
|
||||
name: "Claude 3.5 Haiku",
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.8,
|
||||
output: 4,
|
||||
cacheRead: 0.08,
|
||||
cacheWrite: 1,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"anthropic/claude-fable-5": {
|
||||
id: "anthropic/claude-fable-5",
|
||||
name: "Claude Fable 5",
|
||||
@@ -565,7 +548,7 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
cacheWrite: 18.75,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
maxTokens: 32000,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"anthropic/claude-opus-4.1": {
|
||||
id: "anthropic/claude-opus-4.1",
|
||||
@@ -673,7 +656,7 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
cacheWrite: 3.75,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 64000,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"anthropic/claude-sonnet-4.5": {
|
||||
id: "anthropic/claude-sonnet-4.5",
|
||||
@@ -730,23 +713,6 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"arcee-ai/trinity-large-preview": {
|
||||
id: "arcee-ai/trinity-large-preview",
|
||||
name: "Trinity Large Preview",
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.25,
|
||||
output: 1,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131000,
|
||||
maxTokens: 131000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"arcee-ai/trinity-large-thinking": {
|
||||
id: "arcee-ai/trinity-large-thinking",
|
||||
name: "Trinity Large Thinking",
|
||||
@@ -875,12 +841,12 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.6,
|
||||
output: 1.7,
|
||||
cacheRead: 0,
|
||||
input: 0.21,
|
||||
output: 0.79,
|
||||
cacheRead: 0.13,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
contextWindow: 163840,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"deepseek/deepseek-v3.1-terminus": {
|
||||
@@ -945,7 +911,7 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
cost: {
|
||||
input: 0.14,
|
||||
output: 0.28,
|
||||
cacheRead: 0.0028,
|
||||
cacheRead: 0.028,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
@@ -1206,6 +1172,23 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 32000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"kwaipilot/kat-coder-air-v2.5": {
|
||||
id: "kwaipilot/kat-coder-air-v2.5",
|
||||
name: "Kat Coder Air V2.5",
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.15,
|
||||
output: 0.6,
|
||||
cacheRead: 0.03,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 80000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"kwaipilot/kat-coder-pro-v1": {
|
||||
id: "kwaipilot/kat-coder-pro-v1",
|
||||
name: "KAT-Coder-Pro V1",
|
||||
@@ -1240,39 +1223,22 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
contextWindow: 256000,
|
||||
maxTokens: 256000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"meituan/longcat-flash-chat": {
|
||||
id: "meituan/longcat-flash-chat",
|
||||
name: "LongCat Flash Chat",
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 100000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"meituan/longcat-flash-thinking-2601": {
|
||||
id: "meituan/longcat-flash-thinking-2601",
|
||||
name: "LongCat Flash Thinking 2601",
|
||||
"kwaipilot/kat-coder-pro-v2.5": {
|
||||
id: "kwaipilot/kat-coder-pro-v2.5",
|
||||
name: "Kat Coder Pro V2.5",
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
input: 0.74,
|
||||
output: 2.96,
|
||||
cacheRead: 0.15,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 32768,
|
||||
maxTokens: 32768,
|
||||
contextWindow: 256000,
|
||||
maxTokens: 80000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"meta/llama-3.1-70b": {
|
||||
id: "meta/llama-3.1-70b",
|
||||
@@ -1400,7 +1366,7 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 1.25,
|
||||
output: 4.25,
|
||||
@@ -1580,23 +1546,6 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
contextWindow: 256000,
|
||||
maxTokens: 256000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mistral/devstral-small": {
|
||||
id: "mistral/devstral-small",
|
||||
name: "Devstral Small 1.1",
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.1,
|
||||
output: 0.3,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 64000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mistral/devstral-small-2": {
|
||||
id: "mistral/devstral-small-2",
|
||||
name: "Devstral Small 2",
|
||||
@@ -1801,23 +1750,6 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mistral/pixtral-large": {
|
||||
id: "mistral/pixtral-large",
|
||||
name: "Pixtral Large",
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 2,
|
||||
output: 6,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"moonshotai/kimi-k2": {
|
||||
id: "moonshotai/kimi-k2",
|
||||
name: "Kimi K2 Instruct",
|
||||
@@ -2972,40 +2904,6 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
contextWindow: 256000,
|
||||
maxTokens: 256000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"xiaomi/mimo-v2-flash": {
|
||||
id: "xiaomi/mimo-v2-flash",
|
||||
name: "MiMo V2 Flash",
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.1,
|
||||
output: 0.3,
|
||||
cacheRead: 0.01,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 32000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"xiaomi/mimo-v2-pro": {
|
||||
id: "xiaomi/mimo-v2-pro",
|
||||
name: "MiMo V2 Pro",
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 3,
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"xiaomi/mimo-v2.5": {
|
||||
id: "xiaomi/mimo-v2.5",
|
||||
name: "MiMo M2.5",
|
||||
@@ -3270,9 +3168,9 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 3,
|
||||
output: 10.25,
|
||||
cacheRead: 0.5,
|
||||
input: 2.1,
|
||||
output: 6.6,
|
||||
cacheRead: 0.21,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
/**
|
||||
* OAuth credential management for AI providers.
|
||||
*
|
||||
* This module handles login, token refresh, and credential storage
|
||||
* for OAuth-based providers:
|
||||
* - Anthropic (Claude Pro/Max)
|
||||
* - GitHub Copilot
|
||||
*/
|
||||
|
||||
// Anthropic
|
||||
export { anthropicOAuthProvider, loginAnthropic, refreshAnthropicToken } from "./anthropic.ts";
|
||||
export * from "./device-code.ts";
|
||||
// GitHub Copilot
|
||||
export {
|
||||
getGitHubCopilotBaseUrl,
|
||||
githubCopilotOAuthProvider,
|
||||
loginGitHubCopilot,
|
||||
normalizeDomain,
|
||||
refreshGitHubCopilotToken,
|
||||
} from "./github-copilot.ts";
|
||||
// OpenAI Codex (ChatGPT OAuth)
|
||||
export {
|
||||
loginOpenAICodex,
|
||||
loginOpenAICodexDeviceCode,
|
||||
OPENAI_CODEX_BROWSER_LOGIN_METHOD,
|
||||
OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD,
|
||||
openaiCodexOAuthProvider,
|
||||
refreshOpenAICodexToken,
|
||||
} from "./openai-codex.ts";
|
||||
|
||||
export * from "./types.ts";
|
||||
|
||||
// ============================================================================
|
||||
// Provider Registry
|
||||
// ============================================================================
|
||||
|
||||
import { anthropicOAuthProvider } from "./anthropic.ts";
|
||||
import { githubCopilotOAuthProvider } from "./github-copilot.ts";
|
||||
import { openaiCodexOAuthProvider } from "./openai-codex.ts";
|
||||
import type { OAuthCredentials, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types.ts";
|
||||
|
||||
const BUILT_IN_OAUTH_PROVIDERS: OAuthProviderInterface[] = [
|
||||
anthropicOAuthProvider,
|
||||
githubCopilotOAuthProvider,
|
||||
openaiCodexOAuthProvider,
|
||||
];
|
||||
|
||||
const oauthProviderRegistry = new Map<string, OAuthProviderInterface>(
|
||||
BUILT_IN_OAUTH_PROVIDERS.map((provider) => [provider.id, provider]),
|
||||
);
|
||||
|
||||
/**
|
||||
* Get an OAuth provider by ID
|
||||
*/
|
||||
export function getOAuthProvider(id: OAuthProviderId): OAuthProviderInterface | undefined {
|
||||
return oauthProviderRegistry.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom OAuth provider
|
||||
*/
|
||||
export function registerOAuthProvider(provider: OAuthProviderInterface): void {
|
||||
oauthProviderRegistry.set(provider.id, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an OAuth provider.
|
||||
*
|
||||
* If the provider is built-in, restores the built-in implementation.
|
||||
* Custom providers are removed completely.
|
||||
*/
|
||||
export function unregisterOAuthProvider(id: string): void {
|
||||
const builtInProvider = BUILT_IN_OAUTH_PROVIDERS.find((provider) => provider.id === id);
|
||||
if (builtInProvider) {
|
||||
oauthProviderRegistry.set(id, builtInProvider);
|
||||
return;
|
||||
}
|
||||
oauthProviderRegistry.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset OAuth providers to built-ins.
|
||||
*/
|
||||
export function resetOAuthProviders(): void {
|
||||
oauthProviderRegistry.clear();
|
||||
for (const provider of BUILT_IN_OAUTH_PROVIDERS) {
|
||||
oauthProviderRegistry.set(provider.id, provider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered OAuth providers
|
||||
*/
|
||||
export function getOAuthProviders(): OAuthProviderInterface[] {
|
||||
return Array.from(oauthProviderRegistry.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use getOAuthProviders() which returns OAuthProviderInterface[]
|
||||
*/
|
||||
export function getOAuthProviderInfoList(): OAuthProviderInfo[] {
|
||||
return getOAuthProviders().map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
available: true,
|
||||
}));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// High-level API (uses provider registry)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Refresh token for any OAuth provider.
|
||||
* @deprecated Use getOAuthProvider(id).refreshToken() instead
|
||||
*/
|
||||
export async function refreshOAuthToken(
|
||||
providerId: OAuthProviderId,
|
||||
credentials: OAuthCredentials,
|
||||
): Promise<OAuthCredentials> {
|
||||
const provider = getOAuthProvider(providerId);
|
||||
if (!provider) {
|
||||
throw new Error(`Unknown OAuth provider: ${providerId}`);
|
||||
}
|
||||
return provider.refreshToken(credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key for a provider from OAuth credentials.
|
||||
* Automatically refreshes expired tokens.
|
||||
*
|
||||
* @returns API key string and updated credentials, or null if no credentials
|
||||
* @throws Error if refresh fails
|
||||
*/
|
||||
export async function getOAuthApiKey(
|
||||
providerId: OAuthProviderId,
|
||||
credentials: Record<string, OAuthCredentials>,
|
||||
): Promise<{ newCredentials: OAuthCredentials; apiKey: string } | null> {
|
||||
const provider = getOAuthProvider(providerId);
|
||||
if (!provider) {
|
||||
throw new Error(`Unknown OAuth provider: ${providerId}`);
|
||||
}
|
||||
|
||||
let creds = credentials[providerId];
|
||||
if (!creds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Refresh if expired
|
||||
if (Date.now() >= creds.expires) {
|
||||
try {
|
||||
creds = await provider.refreshToken(creds);
|
||||
} catch (_error) {
|
||||
throw new Error(`Failed to refresh OAuth token for ${providerId}`);
|
||||
}
|
||||
}
|
||||
|
||||
const apiKey = provider.getApiKey(creds);
|
||||
return { newCredentials: creds, apiKey };
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import type { Api, Model } from "../../types.ts";
|
||||
|
||||
export type OAuthCredentials = {
|
||||
refresh: string;
|
||||
access: string;
|
||||
expires: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type OAuthProviderId = string;
|
||||
|
||||
/** @deprecated Use OAuthProviderId instead */
|
||||
export type OAuthProvider = OAuthProviderId;
|
||||
|
||||
export type OAuthPrompt = {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
allowEmpty?: boolean;
|
||||
};
|
||||
|
||||
export type OAuthAuthInfo = {
|
||||
url: string;
|
||||
instructions?: string;
|
||||
};
|
||||
|
||||
export type OAuthDeviceCodeInfo = {
|
||||
userCode: string;
|
||||
verificationUri: string;
|
||||
intervalSeconds?: number;
|
||||
expiresInSeconds?: number;
|
||||
};
|
||||
|
||||
export type OAuthSelectOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type OAuthSelectPrompt = {
|
||||
message: string;
|
||||
options: OAuthSelectOption[];
|
||||
};
|
||||
|
||||
export interface OAuthLoginCallbacks {
|
||||
onAuth: (info: OAuthAuthInfo) => void;
|
||||
onDeviceCode: (info: OAuthDeviceCodeInfo) => void;
|
||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
||||
onProgress?: (message: string) => void;
|
||||
onManualCodeInput?: () => Promise<string>;
|
||||
/** Show an interactive selector and return the selected option id, or undefined on cancel. */
|
||||
onSelect: (prompt: OAuthSelectPrompt) => Promise<string | undefined>;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface OAuthProviderInterface {
|
||||
readonly id: OAuthProviderId;
|
||||
readonly name: string;
|
||||
|
||||
/** Run the login flow, return credentials to persist */
|
||||
login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;
|
||||
|
||||
/** Whether login uses a local callback server and supports manual code input. */
|
||||
usesCallbackServer?: boolean;
|
||||
|
||||
/** Refresh expired credentials, return updated credentials to persist */
|
||||
refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials>;
|
||||
|
||||
/** Convert credentials to API key string for the provider */
|
||||
getApiKey(credentials: OAuthCredentials): string;
|
||||
|
||||
/** Optional: modify models for this provider (e.g., update baseUrl) */
|
||||
modifyModels?(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[];
|
||||
}
|
||||
|
||||
/** @deprecated Use OAuthProviderInterface instead */
|
||||
export interface OAuthProviderInfo {
|
||||
id: OAuthProviderId;
|
||||
name: string;
|
||||
available: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user