@mariozechner/pi-coding-agent 0.27.9 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +16 -17
- package/dist/cli/list-models.d.ts +2 -2
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +2 -7
- package/dist/cli/list-models.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +6 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +18 -20
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +104 -0
- package/dist/core/auth-storage.d.ts.map +1 -0
- package/dist/core/auth-storage.js +232 -0
- package/dist/core/auth-storage.js.map +1 -0
- package/dist/core/model-registry.d.ts +50 -0
- package/dist/core/model-registry.d.ts.map +1 -0
- package/dist/core/model-registry.js +268 -0
- package/dist/core/model-registry.js.map +1 -0
- package/dist/core/model-resolver.d.ts +7 -7
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +12 -44
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts +13 -26
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +24 -101
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +0 -5
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +0 -19
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +15 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -8
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +37 -22
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +3 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +4 -3
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +3 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +21 -14
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts +3 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +6 -6
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +53 -48
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/sdk.md +86 -61
- package/examples/custom-tools/hello/index.ts +15 -15
- package/examples/custom-tools/question/index.ts +3 -3
- package/examples/custom-tools/subagent/agents.ts +1 -2
- package/examples/custom-tools/subagent/index.ts +332 -125
- package/examples/custom-tools/todo/index.ts +30 -12
- package/examples/hooks/confirm-destructive.ts +5 -7
- package/examples/hooks/custom-compaction.ts +7 -7
- package/examples/hooks/dirty-repo-guard.ts +5 -9
- package/examples/hooks/permission-gate.ts +1 -5
- package/examples/sdk/02-custom-model.ts +20 -7
- package/examples/sdk/04-skills.ts +1 -1
- package/examples/sdk/05-tools.ts +11 -14
- package/examples/sdk/06-hooks.ts +1 -1
- package/examples/sdk/07-context-files.ts +1 -1
- package/examples/sdk/08-slash-commands.ts +3 -3
- package/examples/sdk/09-api-keys-and-oauth.ts +36 -26
- package/examples/sdk/10-settings.ts +2 -2
- package/examples/sdk/12-full-control.ts +19 -20
- package/examples/sdk/README.md +26 -13
- package/package.json +4 -5
- package/dist/core/model-config.d.ts +0 -58
- package/dist/core/model-config.d.ts.map +0 -1
- package/dist/core/model-config.js +0 -384
- package/dist/core/model-config.js.map +0 -1
- package/dist/core/oauth/index.d.ts +0 -41
- package/dist/core/oauth/index.d.ts.map +0 -1
- package/dist/core/oauth/index.js +0 -84
- package/dist/core/oauth/index.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mariozechner/pi-coding-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -34,14 +34,13 @@
|
|
|
34
34
|
"build:binary": "npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets",
|
|
35
35
|
"copy-assets": "mkdir -p dist/modes/interactive/theme && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/",
|
|
36
36
|
"copy-binary-assets": "cp package.json dist/ && cp README.md dist/ && cp CHANGELOG.md dist/ && mkdir -p dist/theme && cp src/modes/interactive/theme/*.json dist/theme/ && cp -r docs dist/ && cp -r examples dist/",
|
|
37
|
-
"check": "tsgo --noEmit && tsgo -p tsconfig.examples.json",
|
|
38
37
|
"test": "vitest --run",
|
|
39
38
|
"prepublishOnly": "npm run clean && npm run build"
|
|
40
39
|
},
|
|
41
40
|
"dependencies": {
|
|
42
|
-
"@mariozechner/pi-agent-core": "^0.
|
|
43
|
-
"@mariozechner/pi-ai": "^0.
|
|
44
|
-
"@mariozechner/pi-tui": "^0.
|
|
41
|
+
"@mariozechner/pi-agent-core": "^0.28.0",
|
|
42
|
+
"@mariozechner/pi-ai": "^0.28.0",
|
|
43
|
+
"@mariozechner/pi-tui": "^0.28.0",
|
|
45
44
|
"chalk": "^5.5.0",
|
|
46
45
|
"cli-highlight": "^2.1.11",
|
|
47
46
|
"diff": "^8.0.2",
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { type Api, type Model } from "@mariozechner/pi-ai";
|
|
2
|
-
/**
|
|
3
|
-
* Resolve an API key config value to an actual key.
|
|
4
|
-
* First checks if it's an environment variable, then treats as literal.
|
|
5
|
-
*/
|
|
6
|
-
export declare function resolveApiKey(keyConfig: string): string | undefined;
|
|
7
|
-
/**
|
|
8
|
-
* Get all models (built-in + custom), freshly loaded
|
|
9
|
-
* Returns { models, error } - either models array or error message
|
|
10
|
-
*/
|
|
11
|
-
export declare function loadAndMergeModels(agentDir?: string): {
|
|
12
|
-
models: Model<Api>[];
|
|
13
|
-
error: string | null;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Get API key for a model (checks custom providers first, then built-in)
|
|
17
|
-
* Now async to support OAuth token refresh.
|
|
18
|
-
* Note: OAuth storage location is configured globally via setOAuthStorage.
|
|
19
|
-
*/
|
|
20
|
-
export declare function getApiKeyForModel(model: Model<Api>): Promise<string | undefined>;
|
|
21
|
-
/**
|
|
22
|
-
* Get only models that have valid API keys available
|
|
23
|
-
* Returns { models, error } - either models array or error message
|
|
24
|
-
*
|
|
25
|
-
* @param agentDir - Agent config directory
|
|
26
|
-
* @param fallbackKeyResolver - Optional function to check for API keys not found by getApiKeyForModel
|
|
27
|
-
* (e.g., keys from settings.json)
|
|
28
|
-
*/
|
|
29
|
-
export declare function getAvailableModels(agentDir?: string, fallbackKeyResolver?: (provider: string) => string | undefined): Promise<{
|
|
30
|
-
models: Model<Api>[];
|
|
31
|
-
error: string | null;
|
|
32
|
-
}>;
|
|
33
|
-
/**
|
|
34
|
-
* Find a specific model by provider and ID.
|
|
35
|
-
*
|
|
36
|
-
* Searches models from:
|
|
37
|
-
* 1. Built-in models from @mariozechner/pi-ai
|
|
38
|
-
* 2. Custom models defined in ~/.pi/agent/models.json
|
|
39
|
-
*
|
|
40
|
-
* Returns { model, error } - either the model or an error message.
|
|
41
|
-
*/
|
|
42
|
-
export declare function findModel(provider: string, modelId: string, agentDir?: string): {
|
|
43
|
-
model: Model<Api> | null;
|
|
44
|
-
error: string | null;
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Invalidate the OAuth status cache.
|
|
48
|
-
* Call this after login/logout operations.
|
|
49
|
-
*/
|
|
50
|
-
export declare function invalidateOAuthCache(): void;
|
|
51
|
-
/**
|
|
52
|
-
* Check if a model is using OAuth credentials (subscription).
|
|
53
|
-
* This checks if OAuth credentials exist and would be used for the model,
|
|
54
|
-
* without actually fetching or refreshing the token.
|
|
55
|
-
* Results are cached until invalidateOAuthCache() is called.
|
|
56
|
-
*/
|
|
57
|
-
export declare function isModelUsingOAuth(model: Model<Api>): boolean;
|
|
58
|
-
//# sourceMappingURL=model-config.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"model-config.d.ts","sourceRoot":"","sources":["../../src/core/model-config.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,GAAG,EAOR,KAAK,KAAK,EAKV,MAAM,qBAAqB,CAAC;AAsE7B;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOnE;AAyID;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,GAAE,MAAsB,GAAG;IAAE,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CA+BnH;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAkFtF;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACvC,QAAQ,GAAE,MAAsB,EAChC,mBAAmB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC5D,OAAO,CAAC;IAAE,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAoBzD;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,MAAsB,GAC9B;IAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CASpD;AAgBD;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAyB5D","sourcesContent":["import {\n\ttype Api,\n\tgetApiKey,\n\tgetGitHubCopilotBaseUrl,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\tloadOAuthCredentials,\n\ttype Model,\n\tnormalizeDomain,\n\trefreshGitHubCopilotToken,\n\tremoveOAuthCredentials,\n\tsaveOAuthCredentials,\n} from \"@mariozechner/pi-ai\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport AjvModule from \"ajv\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport { getOAuthToken, type OAuthProvider, refreshToken } from \"./oauth/index.js\";\n\n// Handle both default and named exports\nconst Ajv = (AjvModule as any).default || AjvModule;\n\n// Schema for OpenAI compatibility settings\nconst OpenAICompatSchema = Type.Object({\n\tsupportsStore: Type.Optional(Type.Boolean()),\n\tsupportsDeveloperRole: Type.Optional(Type.Boolean()),\n\tsupportsReasoningEffort: Type.Optional(Type.Boolean()),\n\tmaxTokensField: Type.Optional(Type.Union([Type.Literal(\"max_completion_tokens\"), Type.Literal(\"max_tokens\")])),\n});\n\n// Schema for custom model definition\nconst ModelDefinitionSchema = Type.Object({\n\tid: Type.String({ minLength: 1 }),\n\tname: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\treasoning: Type.Boolean(),\n\tinput: Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")])),\n\tcost: Type.Object({\n\t\tinput: Type.Number(),\n\t\toutput: Type.Number(),\n\t\tcacheRead: Type.Number(),\n\t\tcacheWrite: Type.Number(),\n\t}),\n\tcontextWindow: Type.Number(),\n\tmaxTokens: Type.Number(),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tcompat: Type.Optional(OpenAICompatSchema),\n});\n\nconst ProviderConfigSchema = Type.Object({\n\tbaseUrl: Type.String({ minLength: 1 }),\n\tapiKey: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tauthHeader: Type.Optional(Type.Boolean()),\n\tmodels: Type.Array(ModelDefinitionSchema),\n});\n\nconst ModelsConfigSchema = Type.Object({\n\tproviders: Type.Record(Type.String(), ProviderConfigSchema),\n});\n\ntype ModelsConfig = Static<typeof ModelsConfigSchema>;\n\n// Custom provider API key mappings (provider name -> apiKey config)\nconst customProviderApiKeys: Map<string, string> = new Map();\n\n/**\n * Resolve an API key config value to an actual key.\n * First checks if it's an environment variable, then treats as literal.\n */\nexport function resolveApiKey(keyConfig: string): string | undefined {\n\t// First check if it's an env var name\n\tconst envValue = process.env[keyConfig];\n\tif (envValue) return envValue;\n\n\t// Otherwise treat as literal API key\n\treturn keyConfig;\n}\n\n/**\n * Load custom models from models.json in agent config dir\n * Returns { models, error } - either models array or error message\n */\nfunction loadCustomModels(agentDir: string = getAgentDir()): { models: Model<Api>[]; error: string | null } {\n\tconst configPath = join(agentDir, \"models.json\");\n\tif (!existsSync(configPath)) {\n\t\treturn { models: [], error: null };\n\t}\n\n\ttry {\n\t\tconst content = readFileSync(configPath, \"utf-8\");\n\t\tconst config: ModelsConfig = JSON.parse(content);\n\n\t\t// Validate schema\n\t\tconst ajv = new Ajv();\n\t\tconst validate = ajv.compile(ModelsConfigSchema);\n\t\tif (!validate(config)) {\n\t\t\tconst errors =\n\t\t\t\tvalidate.errors?.map((e: any) => ` - ${e.instancePath || \"root\"}: ${e.message}`).join(\"\\n\") ||\n\t\t\t\t\"Unknown schema error\";\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json schema:\\n${errors}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Additional validation\n\t\ttry {\n\t\t\tvalidateConfig(config);\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Parse models\n\t\treturn { models: parseModels(config), error: null };\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Failed to parse models.json: ${error.message}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tmodels: [],\n\t\t\terror: `Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t};\n\t}\n}\n\n/**\n * Validate config structure and requirements\n */\nfunction validateConfig(config: ModelsConfig): void {\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\tconst hasProviderApi = !!providerConfig.api;\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\tif (!hasProviderApi && !hasModelApi) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. ` +\n\t\t\t\t\t\t`Set at provider or model level.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Validate required fields\n\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\tif (!modelDef.name) throw new Error(`Provider ${providerName}: model missing \"name\"`);\n\t\t\tif (modelDef.contextWindow <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\tif (modelDef.maxTokens <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t}\n\t}\n}\n\n/**\n * Parse config into Model objects\n */\nfunction parseModels(config: ModelsConfig): Model<Api>[] {\n\tconst models: Model<Api>[] = [];\n\n\t// Clear and rebuild custom provider API key mappings\n\tcustomProviderApiKeys.clear();\n\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t// Store API key config for this provider\n\t\tcustomProviderApiKeys.set(providerName, providerConfig.apiKey);\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\t// Model-level api overrides provider-level api\n\t\t\tconst api = modelDef.api || providerConfig.api;\n\n\t\t\tif (!api) {\n\t\t\t\t// This should have been caught by validateConfig, but be safe\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Merge headers: provider headers are base, model headers override\n\t\t\tlet headers =\n\t\t\t\tproviderConfig.headers || modelDef.headers ? { ...providerConfig.headers, ...modelDef.headers } : undefined;\n\n\t\t\t// If authHeader is true, add Authorization header with resolved API key\n\t\t\tif (providerConfig.authHeader) {\n\t\t\t\tconst resolvedKey = resolveApiKey(providerConfig.apiKey);\n\t\t\t\tif (resolvedKey) {\n\t\t\t\t\theaders = { ...headers, Authorization: `Bearer ${resolvedKey}` };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmodels.push({\n\t\t\t\tid: modelDef.id,\n\t\t\t\tname: modelDef.name,\n\t\t\t\tapi: api as Api,\n\t\t\t\tprovider: providerName,\n\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\tcost: modelDef.cost,\n\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\theaders,\n\t\t\t\tcompat: modelDef.compat,\n\t\t\t} as Model<Api>);\n\t\t}\n\t}\n\n\treturn models;\n}\n\n/**\n * Get all models (built-in + custom), freshly loaded\n * Returns { models, error } - either models array or error message\n */\nexport function loadAndMergeModels(agentDir: string = getAgentDir()): { models: Model<Api>[]; error: string | null } {\n\tconst builtInModels: Model<Api>[] = [];\n\tconst providers = getProviders();\n\n\t// Load all built-in models\n\tfor (const provider of providers) {\n\t\tconst providerModels = getModels(provider as KnownProvider);\n\t\tbuiltInModels.push(...(providerModels as Model<Api>[]));\n\t}\n\n\t// Load custom models\n\tconst { models: customModels, error } = loadCustomModels(agentDir);\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\tconst combined = [...builtInModels, ...customModels];\n\n\t// Update github-copilot base URL based on OAuth token or enterprise domain\n\tconst copilotCreds = loadOAuthCredentials(\"github-copilot\");\n\tif (copilotCreds) {\n\t\tconst domain = copilotCreds.enterpriseUrl ? normalizeDomain(copilotCreds.enterpriseUrl) : undefined;\n\t\tconst baseUrl = getGitHubCopilotBaseUrl(copilotCreds.access, domain ?? undefined);\n\t\treturn {\n\t\t\tmodels: combined.map((m) => (m.provider === \"github-copilot\" ? { ...m, baseUrl } : m)),\n\t\t\terror: null,\n\t\t};\n\t}\n\n\treturn { models: combined, error: null };\n}\n\n/**\n * Get API key for a model (checks custom providers first, then built-in)\n * Now async to support OAuth token refresh.\n * Note: OAuth storage location is configured globally via setOAuthStorage.\n */\nexport async function getApiKeyForModel(model: Model<Api>): Promise<string | undefined> {\n\t// For custom providers, check their apiKey config\n\tconst customKeyConfig = customProviderApiKeys.get(model.provider);\n\tif (customKeyConfig) {\n\t\treturn resolveApiKey(customKeyConfig);\n\t}\n\n\t// For Anthropic, check OAuth first\n\tif (model.provider === \"anthropic\") {\n\t\t// 1. Check OAuth storage (auto-refresh if needed)\n\t\tconst oauthToken = await getOAuthToken(\"anthropic\");\n\t\tif (oauthToken) {\n\t\t\treturn oauthToken;\n\t\t}\n\n\t\t// 2. Check ANTHROPIC_OAUTH_TOKEN env var (manual OAuth token)\n\t\tconst oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;\n\t\tif (oauthEnv) {\n\t\t\treturn oauthEnv;\n\t\t}\n\n\t\t// 3. Fall back to ANTHROPIC_API_KEY env var\n\t}\n\n\tif (model.provider === \"github-copilot\") {\n\t\t// 1. Check OAuth storage (from device flow login)\n\t\tconst oauthToken = await getOAuthToken(\"github-copilot\");\n\t\tif (oauthToken) {\n\t\t\treturn oauthToken;\n\t\t}\n\n\t\t// 2. Use GitHub token directly (works with copilot scope on github.com)\n\t\tconst githubToken = process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;\n\t\tif (!githubToken) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// 3. For enterprise, exchange token for short-lived Copilot token\n\t\tconst enterpriseDomain = process.env.COPILOT_ENTERPRISE_URL\n\t\t\t? normalizeDomain(process.env.COPILOT_ENTERPRISE_URL)\n\t\t\t: undefined;\n\n\t\tif (enterpriseDomain) {\n\t\t\tconst creds = await refreshGitHubCopilotToken(githubToken, enterpriseDomain);\n\t\t\tsaveOAuthCredentials(\"github-copilot\", creds);\n\t\t\treturn creds.access;\n\t\t}\n\n\t\t// 4. For github.com, use token directly\n\t\treturn githubToken;\n\t}\n\n\t// For Google Gemini CLI and Antigravity, check OAuth and encode projectId with token\n\tif (model.provider === \"google-gemini-cli\" || model.provider === \"google-antigravity\") {\n\t\tconst oauthProvider = model.provider as \"google-gemini-cli\" | \"google-antigravity\";\n\t\tconst credentials = loadOAuthCredentials(oauthProvider);\n\t\tif (!credentials) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// Check if token is expired\n\t\tif (Date.now() >= credentials.expires) {\n\t\t\ttry {\n\t\t\t\tawait refreshToken(oauthProvider);\n\t\t\t\tconst refreshedCreds = loadOAuthCredentials(oauthProvider);\n\t\t\t\tif (refreshedCreds?.projectId) {\n\t\t\t\t\treturn JSON.stringify({ token: refreshedCreds.access, projectId: refreshedCreds.projectId });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tremoveOAuthCredentials(oauthProvider);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\n\t\tif (credentials.projectId) {\n\t\t\treturn JSON.stringify({ token: credentials.access, projectId: credentials.projectId });\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// For built-in providers, use getApiKey from @mariozechner/pi-ai\n\treturn getApiKey(model.provider as KnownProvider);\n}\n\n/**\n * Get only models that have valid API keys available\n * Returns { models, error } - either models array or error message\n *\n * @param agentDir - Agent config directory\n * @param fallbackKeyResolver - Optional function to check for API keys not found by getApiKeyForModel\n * (e.g., keys from settings.json)\n */\nexport async function getAvailableModels(\n\tagentDir: string = getAgentDir(),\n\tfallbackKeyResolver?: (provider: string) => string | undefined,\n): Promise<{ models: Model<Api>[]; error: string | null }> {\n\tconst { models: allModels, error } = loadAndMergeModels(agentDir);\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\tconst availableModels: Model<Api>[] = [];\n\tfor (const model of allModels) {\n\t\tlet apiKey = await getApiKeyForModel(model);\n\t\t// Check fallback resolver if primary lookup failed\n\t\tif (!apiKey && fallbackKeyResolver) {\n\t\t\tapiKey = fallbackKeyResolver(model.provider);\n\t\t}\n\t\tif (apiKey) {\n\t\t\tavailableModels.push(model);\n\t\t}\n\t}\n\n\treturn { models: availableModels, error: null };\n}\n\n/**\n * Find a specific model by provider and ID.\n *\n * Searches models from:\n * 1. Built-in models from @mariozechner/pi-ai\n * 2. Custom models defined in ~/.pi/agent/models.json\n *\n * Returns { model, error } - either the model or an error message.\n */\nexport function findModel(\n\tprovider: string,\n\tmodelId: string,\n\tagentDir: string = getAgentDir(),\n): { model: Model<Api> | null; error: string | null } {\n\tconst { models: allModels, error } = loadAndMergeModels(agentDir);\n\n\tif (error) {\n\t\treturn { model: null, error };\n\t}\n\n\tconst model = allModels.find((m) => m.provider === provider && m.id === modelId) || null;\n\treturn { model, error: null };\n}\n\n/**\n * Mapping from model provider to OAuth provider ID.\n * Only providers that support OAuth are listed here.\n */\nconst providerToOAuthProvider: Record<string, OAuthProvider> = {\n\tanthropic: \"anthropic\",\n\t\"github-copilot\": \"github-copilot\",\n\t\"google-gemini-cli\": \"google-gemini-cli\",\n\t\"google-antigravity\": \"google-antigravity\",\n};\n\n// Cache for OAuth status per provider (avoids file reads on every render)\nconst oauthStatusCache: Map<string, boolean> = new Map();\n\n/**\n * Invalidate the OAuth status cache.\n * Call this after login/logout operations.\n */\nexport function invalidateOAuthCache(): void {\n\toauthStatusCache.clear();\n}\n\n/**\n * Check if a model is using OAuth credentials (subscription).\n * This checks if OAuth credentials exist and would be used for the model,\n * without actually fetching or refreshing the token.\n * Results are cached until invalidateOAuthCache() is called.\n */\nexport function isModelUsingOAuth(model: Model<Api>): boolean {\n\tconst oauthProvider = providerToOAuthProvider[model.provider];\n\tif (!oauthProvider) {\n\t\treturn false;\n\t}\n\n\t// Check cache first\n\tif (oauthStatusCache.has(oauthProvider)) {\n\t\treturn oauthStatusCache.get(oauthProvider)!;\n\t}\n\n\t// Check if OAuth credentials exist for this provider\n\tlet usingOAuth = false;\n\tconst credentials = loadOAuthCredentials(oauthProvider);\n\tif (credentials) {\n\t\tusingOAuth = true;\n\t}\n\n\t// Also check for manual OAuth token env var (for Anthropic)\n\tif (!usingOAuth && model.provider === \"anthropic\" && process.env.ANTHROPIC_OAUTH_TOKEN) {\n\t\tusingOAuth = true;\n\t}\n\n\toauthStatusCache.set(oauthProvider, usingOAuth);\n\treturn usingOAuth;\n}\n"]}
|
|
@@ -1,384 +0,0 @@
|
|
|
1
|
-
import { getApiKey, getGitHubCopilotBaseUrl, getModels, getProviders, loadOAuthCredentials, normalizeDomain, refreshGitHubCopilotToken, removeOAuthCredentials, saveOAuthCredentials, } from "@mariozechner/pi-ai";
|
|
2
|
-
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import AjvModule from "ajv";
|
|
4
|
-
import { existsSync, readFileSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { getAgentDir } from "../config.js";
|
|
7
|
-
import { getOAuthToken, refreshToken } from "./oauth/index.js";
|
|
8
|
-
// Handle both default and named exports
|
|
9
|
-
const Ajv = AjvModule.default || AjvModule;
|
|
10
|
-
// Schema for OpenAI compatibility settings
|
|
11
|
-
const OpenAICompatSchema = Type.Object({
|
|
12
|
-
supportsStore: Type.Optional(Type.Boolean()),
|
|
13
|
-
supportsDeveloperRole: Type.Optional(Type.Boolean()),
|
|
14
|
-
supportsReasoningEffort: Type.Optional(Type.Boolean()),
|
|
15
|
-
maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
|
|
16
|
-
});
|
|
17
|
-
// Schema for custom model definition
|
|
18
|
-
const ModelDefinitionSchema = Type.Object({
|
|
19
|
-
id: Type.String({ minLength: 1 }),
|
|
20
|
-
name: Type.String({ minLength: 1 }),
|
|
21
|
-
api: Type.Optional(Type.Union([
|
|
22
|
-
Type.Literal("openai-completions"),
|
|
23
|
-
Type.Literal("openai-responses"),
|
|
24
|
-
Type.Literal("anthropic-messages"),
|
|
25
|
-
Type.Literal("google-generative-ai"),
|
|
26
|
-
])),
|
|
27
|
-
reasoning: Type.Boolean(),
|
|
28
|
-
input: Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])),
|
|
29
|
-
cost: Type.Object({
|
|
30
|
-
input: Type.Number(),
|
|
31
|
-
output: Type.Number(),
|
|
32
|
-
cacheRead: Type.Number(),
|
|
33
|
-
cacheWrite: Type.Number(),
|
|
34
|
-
}),
|
|
35
|
-
contextWindow: Type.Number(),
|
|
36
|
-
maxTokens: Type.Number(),
|
|
37
|
-
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
38
|
-
compat: Type.Optional(OpenAICompatSchema),
|
|
39
|
-
});
|
|
40
|
-
const ProviderConfigSchema = Type.Object({
|
|
41
|
-
baseUrl: Type.String({ minLength: 1 }),
|
|
42
|
-
apiKey: Type.String({ minLength: 1 }),
|
|
43
|
-
api: Type.Optional(Type.Union([
|
|
44
|
-
Type.Literal("openai-completions"),
|
|
45
|
-
Type.Literal("openai-responses"),
|
|
46
|
-
Type.Literal("anthropic-messages"),
|
|
47
|
-
Type.Literal("google-generative-ai"),
|
|
48
|
-
])),
|
|
49
|
-
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
50
|
-
authHeader: Type.Optional(Type.Boolean()),
|
|
51
|
-
models: Type.Array(ModelDefinitionSchema),
|
|
52
|
-
});
|
|
53
|
-
const ModelsConfigSchema = Type.Object({
|
|
54
|
-
providers: Type.Record(Type.String(), ProviderConfigSchema),
|
|
55
|
-
});
|
|
56
|
-
// Custom provider API key mappings (provider name -> apiKey config)
|
|
57
|
-
const customProviderApiKeys = new Map();
|
|
58
|
-
/**
|
|
59
|
-
* Resolve an API key config value to an actual key.
|
|
60
|
-
* First checks if it's an environment variable, then treats as literal.
|
|
61
|
-
*/
|
|
62
|
-
export function resolveApiKey(keyConfig) {
|
|
63
|
-
// First check if it's an env var name
|
|
64
|
-
const envValue = process.env[keyConfig];
|
|
65
|
-
if (envValue)
|
|
66
|
-
return envValue;
|
|
67
|
-
// Otherwise treat as literal API key
|
|
68
|
-
return keyConfig;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Load custom models from models.json in agent config dir
|
|
72
|
-
* Returns { models, error } - either models array or error message
|
|
73
|
-
*/
|
|
74
|
-
function loadCustomModels(agentDir = getAgentDir()) {
|
|
75
|
-
const configPath = join(agentDir, "models.json");
|
|
76
|
-
if (!existsSync(configPath)) {
|
|
77
|
-
return { models: [], error: null };
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
const content = readFileSync(configPath, "utf-8");
|
|
81
|
-
const config = JSON.parse(content);
|
|
82
|
-
// Validate schema
|
|
83
|
-
const ajv = new Ajv();
|
|
84
|
-
const validate = ajv.compile(ModelsConfigSchema);
|
|
85
|
-
if (!validate(config)) {
|
|
86
|
-
const errors = validate.errors?.map((e) => ` - ${e.instancePath || "root"}: ${e.message}`).join("\n") ||
|
|
87
|
-
"Unknown schema error";
|
|
88
|
-
return {
|
|
89
|
-
models: [],
|
|
90
|
-
error: `Invalid models.json schema:\n${errors}\n\nFile: ${configPath}`,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
// Additional validation
|
|
94
|
-
try {
|
|
95
|
-
validateConfig(config);
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
return {
|
|
99
|
-
models: [],
|
|
100
|
-
error: `Invalid models.json: ${error instanceof Error ? error.message : error}\n\nFile: ${configPath}`,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
// Parse models
|
|
104
|
-
return { models: parseModels(config), error: null };
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
if (error instanceof SyntaxError) {
|
|
108
|
-
return {
|
|
109
|
-
models: [],
|
|
110
|
-
error: `Failed to parse models.json: ${error.message}\n\nFile: ${configPath}`,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
return {
|
|
114
|
-
models: [],
|
|
115
|
-
error: `Failed to load models.json: ${error instanceof Error ? error.message : error}\n\nFile: ${configPath}`,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Validate config structure and requirements
|
|
121
|
-
*/
|
|
122
|
-
function validateConfig(config) {
|
|
123
|
-
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
|
124
|
-
const hasProviderApi = !!providerConfig.api;
|
|
125
|
-
for (const modelDef of providerConfig.models) {
|
|
126
|
-
const hasModelApi = !!modelDef.api;
|
|
127
|
-
if (!hasProviderApi && !hasModelApi) {
|
|
128
|
-
throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified. ` +
|
|
129
|
-
`Set at provider or model level.`);
|
|
130
|
-
}
|
|
131
|
-
// Validate required fields
|
|
132
|
-
if (!modelDef.id)
|
|
133
|
-
throw new Error(`Provider ${providerName}: model missing "id"`);
|
|
134
|
-
if (!modelDef.name)
|
|
135
|
-
throw new Error(`Provider ${providerName}: model missing "name"`);
|
|
136
|
-
if (modelDef.contextWindow <= 0)
|
|
137
|
-
throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
|
|
138
|
-
if (modelDef.maxTokens <= 0)
|
|
139
|
-
throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Parse config into Model objects
|
|
145
|
-
*/
|
|
146
|
-
function parseModels(config) {
|
|
147
|
-
const models = [];
|
|
148
|
-
// Clear and rebuild custom provider API key mappings
|
|
149
|
-
customProviderApiKeys.clear();
|
|
150
|
-
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
|
151
|
-
// Store API key config for this provider
|
|
152
|
-
customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
|
153
|
-
for (const modelDef of providerConfig.models) {
|
|
154
|
-
// Model-level api overrides provider-level api
|
|
155
|
-
const api = modelDef.api || providerConfig.api;
|
|
156
|
-
if (!api) {
|
|
157
|
-
// This should have been caught by validateConfig, but be safe
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
// Merge headers: provider headers are base, model headers override
|
|
161
|
-
let headers = providerConfig.headers || modelDef.headers ? { ...providerConfig.headers, ...modelDef.headers } : undefined;
|
|
162
|
-
// If authHeader is true, add Authorization header with resolved API key
|
|
163
|
-
if (providerConfig.authHeader) {
|
|
164
|
-
const resolvedKey = resolveApiKey(providerConfig.apiKey);
|
|
165
|
-
if (resolvedKey) {
|
|
166
|
-
headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
models.push({
|
|
170
|
-
id: modelDef.id,
|
|
171
|
-
name: modelDef.name,
|
|
172
|
-
api: api,
|
|
173
|
-
provider: providerName,
|
|
174
|
-
baseUrl: providerConfig.baseUrl,
|
|
175
|
-
reasoning: modelDef.reasoning,
|
|
176
|
-
input: modelDef.input,
|
|
177
|
-
cost: modelDef.cost,
|
|
178
|
-
contextWindow: modelDef.contextWindow,
|
|
179
|
-
maxTokens: modelDef.maxTokens,
|
|
180
|
-
headers,
|
|
181
|
-
compat: modelDef.compat,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return models;
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Get all models (built-in + custom), freshly loaded
|
|
189
|
-
* Returns { models, error } - either models array or error message
|
|
190
|
-
*/
|
|
191
|
-
export function loadAndMergeModels(agentDir = getAgentDir()) {
|
|
192
|
-
const builtInModels = [];
|
|
193
|
-
const providers = getProviders();
|
|
194
|
-
// Load all built-in models
|
|
195
|
-
for (const provider of providers) {
|
|
196
|
-
const providerModels = getModels(provider);
|
|
197
|
-
builtInModels.push(...providerModels);
|
|
198
|
-
}
|
|
199
|
-
// Load custom models
|
|
200
|
-
const { models: customModels, error } = loadCustomModels(agentDir);
|
|
201
|
-
if (error) {
|
|
202
|
-
return { models: [], error };
|
|
203
|
-
}
|
|
204
|
-
const combined = [...builtInModels, ...customModels];
|
|
205
|
-
// Update github-copilot base URL based on OAuth token or enterprise domain
|
|
206
|
-
const copilotCreds = loadOAuthCredentials("github-copilot");
|
|
207
|
-
if (copilotCreds) {
|
|
208
|
-
const domain = copilotCreds.enterpriseUrl ? normalizeDomain(copilotCreds.enterpriseUrl) : undefined;
|
|
209
|
-
const baseUrl = getGitHubCopilotBaseUrl(copilotCreds.access, domain ?? undefined);
|
|
210
|
-
return {
|
|
211
|
-
models: combined.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m)),
|
|
212
|
-
error: null,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
return { models: combined, error: null };
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Get API key for a model (checks custom providers first, then built-in)
|
|
219
|
-
* Now async to support OAuth token refresh.
|
|
220
|
-
* Note: OAuth storage location is configured globally via setOAuthStorage.
|
|
221
|
-
*/
|
|
222
|
-
export async function getApiKeyForModel(model) {
|
|
223
|
-
// For custom providers, check their apiKey config
|
|
224
|
-
const customKeyConfig = customProviderApiKeys.get(model.provider);
|
|
225
|
-
if (customKeyConfig) {
|
|
226
|
-
return resolveApiKey(customKeyConfig);
|
|
227
|
-
}
|
|
228
|
-
// For Anthropic, check OAuth first
|
|
229
|
-
if (model.provider === "anthropic") {
|
|
230
|
-
// 1. Check OAuth storage (auto-refresh if needed)
|
|
231
|
-
const oauthToken = await getOAuthToken("anthropic");
|
|
232
|
-
if (oauthToken) {
|
|
233
|
-
return oauthToken;
|
|
234
|
-
}
|
|
235
|
-
// 2. Check ANTHROPIC_OAUTH_TOKEN env var (manual OAuth token)
|
|
236
|
-
const oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;
|
|
237
|
-
if (oauthEnv) {
|
|
238
|
-
return oauthEnv;
|
|
239
|
-
}
|
|
240
|
-
// 3. Fall back to ANTHROPIC_API_KEY env var
|
|
241
|
-
}
|
|
242
|
-
if (model.provider === "github-copilot") {
|
|
243
|
-
// 1. Check OAuth storage (from device flow login)
|
|
244
|
-
const oauthToken = await getOAuthToken("github-copilot");
|
|
245
|
-
if (oauthToken) {
|
|
246
|
-
return oauthToken;
|
|
247
|
-
}
|
|
248
|
-
// 2. Use GitHub token directly (works with copilot scope on github.com)
|
|
249
|
-
const githubToken = process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
250
|
-
if (!githubToken) {
|
|
251
|
-
return undefined;
|
|
252
|
-
}
|
|
253
|
-
// 3. For enterprise, exchange token for short-lived Copilot token
|
|
254
|
-
const enterpriseDomain = process.env.COPILOT_ENTERPRISE_URL
|
|
255
|
-
? normalizeDomain(process.env.COPILOT_ENTERPRISE_URL)
|
|
256
|
-
: undefined;
|
|
257
|
-
if (enterpriseDomain) {
|
|
258
|
-
const creds = await refreshGitHubCopilotToken(githubToken, enterpriseDomain);
|
|
259
|
-
saveOAuthCredentials("github-copilot", creds);
|
|
260
|
-
return creds.access;
|
|
261
|
-
}
|
|
262
|
-
// 4. For github.com, use token directly
|
|
263
|
-
return githubToken;
|
|
264
|
-
}
|
|
265
|
-
// For Google Gemini CLI and Antigravity, check OAuth and encode projectId with token
|
|
266
|
-
if (model.provider === "google-gemini-cli" || model.provider === "google-antigravity") {
|
|
267
|
-
const oauthProvider = model.provider;
|
|
268
|
-
const credentials = loadOAuthCredentials(oauthProvider);
|
|
269
|
-
if (!credentials) {
|
|
270
|
-
return undefined;
|
|
271
|
-
}
|
|
272
|
-
// Check if token is expired
|
|
273
|
-
if (Date.now() >= credentials.expires) {
|
|
274
|
-
try {
|
|
275
|
-
await refreshToken(oauthProvider);
|
|
276
|
-
const refreshedCreds = loadOAuthCredentials(oauthProvider);
|
|
277
|
-
if (refreshedCreds?.projectId) {
|
|
278
|
-
return JSON.stringify({ token: refreshedCreds.access, projectId: refreshedCreds.projectId });
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
catch {
|
|
282
|
-
removeOAuthCredentials(oauthProvider);
|
|
283
|
-
return undefined;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (credentials.projectId) {
|
|
287
|
-
return JSON.stringify({ token: credentials.access, projectId: credentials.projectId });
|
|
288
|
-
}
|
|
289
|
-
return undefined;
|
|
290
|
-
}
|
|
291
|
-
// For built-in providers, use getApiKey from @mariozechner/pi-ai
|
|
292
|
-
return getApiKey(model.provider);
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Get only models that have valid API keys available
|
|
296
|
-
* Returns { models, error } - either models array or error message
|
|
297
|
-
*
|
|
298
|
-
* @param agentDir - Agent config directory
|
|
299
|
-
* @param fallbackKeyResolver - Optional function to check for API keys not found by getApiKeyForModel
|
|
300
|
-
* (e.g., keys from settings.json)
|
|
301
|
-
*/
|
|
302
|
-
export async function getAvailableModels(agentDir = getAgentDir(), fallbackKeyResolver) {
|
|
303
|
-
const { models: allModels, error } = loadAndMergeModels(agentDir);
|
|
304
|
-
if (error) {
|
|
305
|
-
return { models: [], error };
|
|
306
|
-
}
|
|
307
|
-
const availableModels = [];
|
|
308
|
-
for (const model of allModels) {
|
|
309
|
-
let apiKey = await getApiKeyForModel(model);
|
|
310
|
-
// Check fallback resolver if primary lookup failed
|
|
311
|
-
if (!apiKey && fallbackKeyResolver) {
|
|
312
|
-
apiKey = fallbackKeyResolver(model.provider);
|
|
313
|
-
}
|
|
314
|
-
if (apiKey) {
|
|
315
|
-
availableModels.push(model);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return { models: availableModels, error: null };
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Find a specific model by provider and ID.
|
|
322
|
-
*
|
|
323
|
-
* Searches models from:
|
|
324
|
-
* 1. Built-in models from @mariozechner/pi-ai
|
|
325
|
-
* 2. Custom models defined in ~/.pi/agent/models.json
|
|
326
|
-
*
|
|
327
|
-
* Returns { model, error } - either the model or an error message.
|
|
328
|
-
*/
|
|
329
|
-
export function findModel(provider, modelId, agentDir = getAgentDir()) {
|
|
330
|
-
const { models: allModels, error } = loadAndMergeModels(agentDir);
|
|
331
|
-
if (error) {
|
|
332
|
-
return { model: null, error };
|
|
333
|
-
}
|
|
334
|
-
const model = allModels.find((m) => m.provider === provider && m.id === modelId) || null;
|
|
335
|
-
return { model, error: null };
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Mapping from model provider to OAuth provider ID.
|
|
339
|
-
* Only providers that support OAuth are listed here.
|
|
340
|
-
*/
|
|
341
|
-
const providerToOAuthProvider = {
|
|
342
|
-
anthropic: "anthropic",
|
|
343
|
-
"github-copilot": "github-copilot",
|
|
344
|
-
"google-gemini-cli": "google-gemini-cli",
|
|
345
|
-
"google-antigravity": "google-antigravity",
|
|
346
|
-
};
|
|
347
|
-
// Cache for OAuth status per provider (avoids file reads on every render)
|
|
348
|
-
const oauthStatusCache = new Map();
|
|
349
|
-
/**
|
|
350
|
-
* Invalidate the OAuth status cache.
|
|
351
|
-
* Call this after login/logout operations.
|
|
352
|
-
*/
|
|
353
|
-
export function invalidateOAuthCache() {
|
|
354
|
-
oauthStatusCache.clear();
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Check if a model is using OAuth credentials (subscription).
|
|
358
|
-
* This checks if OAuth credentials exist and would be used for the model,
|
|
359
|
-
* without actually fetching or refreshing the token.
|
|
360
|
-
* Results are cached until invalidateOAuthCache() is called.
|
|
361
|
-
*/
|
|
362
|
-
export function isModelUsingOAuth(model) {
|
|
363
|
-
const oauthProvider = providerToOAuthProvider[model.provider];
|
|
364
|
-
if (!oauthProvider) {
|
|
365
|
-
return false;
|
|
366
|
-
}
|
|
367
|
-
// Check cache first
|
|
368
|
-
if (oauthStatusCache.has(oauthProvider)) {
|
|
369
|
-
return oauthStatusCache.get(oauthProvider);
|
|
370
|
-
}
|
|
371
|
-
// Check if OAuth credentials exist for this provider
|
|
372
|
-
let usingOAuth = false;
|
|
373
|
-
const credentials = loadOAuthCredentials(oauthProvider);
|
|
374
|
-
if (credentials) {
|
|
375
|
-
usingOAuth = true;
|
|
376
|
-
}
|
|
377
|
-
// Also check for manual OAuth token env var (for Anthropic)
|
|
378
|
-
if (!usingOAuth && model.provider === "anthropic" && process.env.ANTHROPIC_OAUTH_TOKEN) {
|
|
379
|
-
usingOAuth = true;
|
|
380
|
-
}
|
|
381
|
-
oauthStatusCache.set(oauthProvider, usingOAuth);
|
|
382
|
-
return usingOAuth;
|
|
383
|
-
}
|
|
384
|
-
//# sourceMappingURL=model-config.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"model-config.js","sourceRoot":"","sources":["../../src/core/model-config.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Api,\n\tgetApiKey,\n\tgetGitHubCopilotBaseUrl,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\tloadOAuthCredentials,\n\ttype Model,\n\tnormalizeDomain,\n\trefreshGitHubCopilotToken,\n\tremoveOAuthCredentials,\n\tsaveOAuthCredentials,\n} from \"@mariozechner/pi-ai\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport AjvModule from \"ajv\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport { getOAuthToken, type OAuthProvider, refreshToken } from \"./oauth/index.js\";\n\n// Handle both default and named exports\nconst Ajv = (AjvModule as any).default || AjvModule;\n\n// Schema for OpenAI compatibility settings\nconst OpenAICompatSchema = Type.Object({\n\tsupportsStore: Type.Optional(Type.Boolean()),\n\tsupportsDeveloperRole: Type.Optional(Type.Boolean()),\n\tsupportsReasoningEffort: Type.Optional(Type.Boolean()),\n\tmaxTokensField: Type.Optional(Type.Union([Type.Literal(\"max_completion_tokens\"), Type.Literal(\"max_tokens\")])),\n});\n\n// Schema for custom model definition\nconst ModelDefinitionSchema = Type.Object({\n\tid: Type.String({ minLength: 1 }),\n\tname: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\treasoning: Type.Boolean(),\n\tinput: Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")])),\n\tcost: Type.Object({\n\t\tinput: Type.Number(),\n\t\toutput: Type.Number(),\n\t\tcacheRead: Type.Number(),\n\t\tcacheWrite: Type.Number(),\n\t}),\n\tcontextWindow: Type.Number(),\n\tmaxTokens: Type.Number(),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tcompat: Type.Optional(OpenAICompatSchema),\n});\n\nconst ProviderConfigSchema = Type.Object({\n\tbaseUrl: Type.String({ minLength: 1 }),\n\tapiKey: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tauthHeader: Type.Optional(Type.Boolean()),\n\tmodels: Type.Array(ModelDefinitionSchema),\n});\n\nconst ModelsConfigSchema = Type.Object({\n\tproviders: Type.Record(Type.String(), ProviderConfigSchema),\n});\n\ntype ModelsConfig = Static<typeof ModelsConfigSchema>;\n\n// Custom provider API key mappings (provider name -> apiKey config)\nconst customProviderApiKeys: Map<string, string> = new Map();\n\n/**\n * Resolve an API key config value to an actual key.\n * First checks if it's an environment variable, then treats as literal.\n */\nexport function resolveApiKey(keyConfig: string): string | undefined {\n\t// First check if it's an env var name\n\tconst envValue = process.env[keyConfig];\n\tif (envValue) return envValue;\n\n\t// Otherwise treat as literal API key\n\treturn keyConfig;\n}\n\n/**\n * Load custom models from models.json in agent config dir\n * Returns { models, error } - either models array or error message\n */\nfunction loadCustomModels(agentDir: string = getAgentDir()): { models: Model<Api>[]; error: string | null } {\n\tconst configPath = join(agentDir, \"models.json\");\n\tif (!existsSync(configPath)) {\n\t\treturn { models: [], error: null };\n\t}\n\n\ttry {\n\t\tconst content = readFileSync(configPath, \"utf-8\");\n\t\tconst config: ModelsConfig = JSON.parse(content);\n\n\t\t// Validate schema\n\t\tconst ajv = new Ajv();\n\t\tconst validate = ajv.compile(ModelsConfigSchema);\n\t\tif (!validate(config)) {\n\t\t\tconst errors =\n\t\t\t\tvalidate.errors?.map((e: any) => ` - ${e.instancePath || \"root\"}: ${e.message}`).join(\"\\n\") ||\n\t\t\t\t\"Unknown schema error\";\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json schema:\\n${errors}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Additional validation\n\t\ttry {\n\t\t\tvalidateConfig(config);\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Invalid models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\n\t\t// Parse models\n\t\treturn { models: parseModels(config), error: null };\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Failed to parse models.json: ${error.message}\\n\\nFile: ${configPath}`,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tmodels: [],\n\t\t\terror: `Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${configPath}`,\n\t\t};\n\t}\n}\n\n/**\n * Validate config structure and requirements\n */\nfunction validateConfig(config: ModelsConfig): void {\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\tconst hasProviderApi = !!providerConfig.api;\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\tif (!hasProviderApi && !hasModelApi) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. ` +\n\t\t\t\t\t\t`Set at provider or model level.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Validate required fields\n\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\tif (!modelDef.name) throw new Error(`Provider ${providerName}: model missing \"name\"`);\n\t\t\tif (modelDef.contextWindow <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\tif (modelDef.maxTokens <= 0)\n\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t}\n\t}\n}\n\n/**\n * Parse config into Model objects\n */\nfunction parseModels(config: ModelsConfig): Model<Api>[] {\n\tconst models: Model<Api>[] = [];\n\n\t// Clear and rebuild custom provider API key mappings\n\tcustomProviderApiKeys.clear();\n\n\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t// Store API key config for this provider\n\t\tcustomProviderApiKeys.set(providerName, providerConfig.apiKey);\n\n\t\tfor (const modelDef of providerConfig.models) {\n\t\t\t// Model-level api overrides provider-level api\n\t\t\tconst api = modelDef.api || providerConfig.api;\n\n\t\t\tif (!api) {\n\t\t\t\t// This should have been caught by validateConfig, but be safe\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Merge headers: provider headers are base, model headers override\n\t\t\tlet headers =\n\t\t\t\tproviderConfig.headers || modelDef.headers ? { ...providerConfig.headers, ...modelDef.headers } : undefined;\n\n\t\t\t// If authHeader is true, add Authorization header with resolved API key\n\t\t\tif (providerConfig.authHeader) {\n\t\t\t\tconst resolvedKey = resolveApiKey(providerConfig.apiKey);\n\t\t\t\tif (resolvedKey) {\n\t\t\t\t\theaders = { ...headers, Authorization: `Bearer ${resolvedKey}` };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmodels.push({\n\t\t\t\tid: modelDef.id,\n\t\t\t\tname: modelDef.name,\n\t\t\t\tapi: api as Api,\n\t\t\t\tprovider: providerName,\n\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\tcost: modelDef.cost,\n\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\theaders,\n\t\t\t\tcompat: modelDef.compat,\n\t\t\t} as Model<Api>);\n\t\t}\n\t}\n\n\treturn models;\n}\n\n/**\n * Get all models (built-in + custom), freshly loaded\n * Returns { models, error } - either models array or error message\n */\nexport function loadAndMergeModels(agentDir: string = getAgentDir()): { models: Model<Api>[]; error: string | null } {\n\tconst builtInModels: Model<Api>[] = [];\n\tconst providers = getProviders();\n\n\t// Load all built-in models\n\tfor (const provider of providers) {\n\t\tconst providerModels = getModels(provider as KnownProvider);\n\t\tbuiltInModels.push(...(providerModels as Model<Api>[]));\n\t}\n\n\t// Load custom models\n\tconst { models: customModels, error } = loadCustomModels(agentDir);\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\tconst combined = [...builtInModels, ...customModels];\n\n\t// Update github-copilot base URL based on OAuth token or enterprise domain\n\tconst copilotCreds = loadOAuthCredentials(\"github-copilot\");\n\tif (copilotCreds) {\n\t\tconst domain = copilotCreds.enterpriseUrl ? normalizeDomain(copilotCreds.enterpriseUrl) : undefined;\n\t\tconst baseUrl = getGitHubCopilotBaseUrl(copilotCreds.access, domain ?? undefined);\n\t\treturn {\n\t\t\tmodels: combined.map((m) => (m.provider === \"github-copilot\" ? { ...m, baseUrl } : m)),\n\t\t\terror: null,\n\t\t};\n\t}\n\n\treturn { models: combined, error: null };\n}\n\n/**\n * Get API key for a model (checks custom providers first, then built-in)\n * Now async to support OAuth token refresh.\n * Note: OAuth storage location is configured globally via setOAuthStorage.\n */\nexport async function getApiKeyForModel(model: Model<Api>): Promise<string | undefined> {\n\t// For custom providers, check their apiKey config\n\tconst customKeyConfig = customProviderApiKeys.get(model.provider);\n\tif (customKeyConfig) {\n\t\treturn resolveApiKey(customKeyConfig);\n\t}\n\n\t// For Anthropic, check OAuth first\n\tif (model.provider === \"anthropic\") {\n\t\t// 1. Check OAuth storage (auto-refresh if needed)\n\t\tconst oauthToken = await getOAuthToken(\"anthropic\");\n\t\tif (oauthToken) {\n\t\t\treturn oauthToken;\n\t\t}\n\n\t\t// 2. Check ANTHROPIC_OAUTH_TOKEN env var (manual OAuth token)\n\t\tconst oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;\n\t\tif (oauthEnv) {\n\t\t\treturn oauthEnv;\n\t\t}\n\n\t\t// 3. Fall back to ANTHROPIC_API_KEY env var\n\t}\n\n\tif (model.provider === \"github-copilot\") {\n\t\t// 1. Check OAuth storage (from device flow login)\n\t\tconst oauthToken = await getOAuthToken(\"github-copilot\");\n\t\tif (oauthToken) {\n\t\t\treturn oauthToken;\n\t\t}\n\n\t\t// 2. Use GitHub token directly (works with copilot scope on github.com)\n\t\tconst githubToken = process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;\n\t\tif (!githubToken) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// 3. For enterprise, exchange token for short-lived Copilot token\n\t\tconst enterpriseDomain = process.env.COPILOT_ENTERPRISE_URL\n\t\t\t? normalizeDomain(process.env.COPILOT_ENTERPRISE_URL)\n\t\t\t: undefined;\n\n\t\tif (enterpriseDomain) {\n\t\t\tconst creds = await refreshGitHubCopilotToken(githubToken, enterpriseDomain);\n\t\t\tsaveOAuthCredentials(\"github-copilot\", creds);\n\t\t\treturn creds.access;\n\t\t}\n\n\t\t// 4. For github.com, use token directly\n\t\treturn githubToken;\n\t}\n\n\t// For Google Gemini CLI and Antigravity, check OAuth and encode projectId with token\n\tif (model.provider === \"google-gemini-cli\" || model.provider === \"google-antigravity\") {\n\t\tconst oauthProvider = model.provider as \"google-gemini-cli\" | \"google-antigravity\";\n\t\tconst credentials = loadOAuthCredentials(oauthProvider);\n\t\tif (!credentials) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// Check if token is expired\n\t\tif (Date.now() >= credentials.expires) {\n\t\t\ttry {\n\t\t\t\tawait refreshToken(oauthProvider);\n\t\t\t\tconst refreshedCreds = loadOAuthCredentials(oauthProvider);\n\t\t\t\tif (refreshedCreds?.projectId) {\n\t\t\t\t\treturn JSON.stringify({ token: refreshedCreds.access, projectId: refreshedCreds.projectId });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tremoveOAuthCredentials(oauthProvider);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\n\t\tif (credentials.projectId) {\n\t\t\treturn JSON.stringify({ token: credentials.access, projectId: credentials.projectId });\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// For built-in providers, use getApiKey from @mariozechner/pi-ai\n\treturn getApiKey(model.provider as KnownProvider);\n}\n\n/**\n * Get only models that have valid API keys available\n * Returns { models, error } - either models array or error message\n *\n * @param agentDir - Agent config directory\n * @param fallbackKeyResolver - Optional function to check for API keys not found by getApiKeyForModel\n * (e.g., keys from settings.json)\n */\nexport async function getAvailableModels(\n\tagentDir: string = getAgentDir(),\n\tfallbackKeyResolver?: (provider: string) => string | undefined,\n): Promise<{ models: Model<Api>[]; error: string | null }> {\n\tconst { models: allModels, error } = loadAndMergeModels(agentDir);\n\n\tif (error) {\n\t\treturn { models: [], error };\n\t}\n\n\tconst availableModels: Model<Api>[] = [];\n\tfor (const model of allModels) {\n\t\tlet apiKey = await getApiKeyForModel(model);\n\t\t// Check fallback resolver if primary lookup failed\n\t\tif (!apiKey && fallbackKeyResolver) {\n\t\t\tapiKey = fallbackKeyResolver(model.provider);\n\t\t}\n\t\tif (apiKey) {\n\t\t\tavailableModels.push(model);\n\t\t}\n\t}\n\n\treturn { models: availableModels, error: null };\n}\n\n/**\n * Find a specific model by provider and ID.\n *\n * Searches models from:\n * 1. Built-in models from @mariozechner/pi-ai\n * 2. Custom models defined in ~/.pi/agent/models.json\n *\n * Returns { model, error } - either the model or an error message.\n */\nexport function findModel(\n\tprovider: string,\n\tmodelId: string,\n\tagentDir: string = getAgentDir(),\n): { model: Model<Api> | null; error: string | null } {\n\tconst { models: allModels, error } = loadAndMergeModels(agentDir);\n\n\tif (error) {\n\t\treturn { model: null, error };\n\t}\n\n\tconst model = allModels.find((m) => m.provider === provider && m.id === modelId) || null;\n\treturn { model, error: null };\n}\n\n/**\n * Mapping from model provider to OAuth provider ID.\n * Only providers that support OAuth are listed here.\n */\nconst providerToOAuthProvider: Record<string, OAuthProvider> = {\n\tanthropic: \"anthropic\",\n\t\"github-copilot\": \"github-copilot\",\n\t\"google-gemini-cli\": \"google-gemini-cli\",\n\t\"google-antigravity\": \"google-antigravity\",\n};\n\n// Cache for OAuth status per provider (avoids file reads on every render)\nconst oauthStatusCache: Map<string, boolean> = new Map();\n\n/**\n * Invalidate the OAuth status cache.\n * Call this after login/logout operations.\n */\nexport function invalidateOAuthCache(): void {\n\toauthStatusCache.clear();\n}\n\n/**\n * Check if a model is using OAuth credentials (subscription).\n * This checks if OAuth credentials exist and would be used for the model,\n * without actually fetching or refreshing the token.\n * Results are cached until invalidateOAuthCache() is called.\n */\nexport function isModelUsingOAuth(model: Model<Api>): boolean {\n\tconst oauthProvider = providerToOAuthProvider[model.provider];\n\tif (!oauthProvider) {\n\t\treturn false;\n\t}\n\n\t// Check cache first\n\tif (oauthStatusCache.has(oauthProvider)) {\n\t\treturn oauthStatusCache.get(oauthProvider)!;\n\t}\n\n\t// Check if OAuth credentials exist for this provider\n\tlet usingOAuth = false;\n\tconst credentials = loadOAuthCredentials(oauthProvider);\n\tif (credentials) {\n\t\tusingOAuth = true;\n\t}\n\n\t// Also check for manual OAuth token env var (for Anthropic)\n\tif (!usingOAuth && model.provider === \"anthropic\" && process.env.ANTHROPIC_OAUTH_TOKEN) {\n\t\tusingOAuth = true;\n\t}\n\n\toauthStatusCache.set(oauthProvider, usingOAuth);\n\treturn usingOAuth;\n}\n"]}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OAuth management for coding-agent.
|
|
3
|
-
* Re-exports from @mariozechner/pi-ai and adds convenience wrappers.
|
|
4
|
-
*/
|
|
5
|
-
import { getOAuthApiKey, listOAuthProviders as listOAuthProvidersFromAi, loadOAuthCredentials, type OAuthCredentials, type OAuthProvider, type OAuthStorageBackend, removeOAuthCredentials, resetOAuthStorage, saveOAuthCredentials, setOAuthStorage } from "@mariozechner/pi-ai";
|
|
6
|
-
export type { OAuthCredentials, OAuthProvider, OAuthStorageBackend };
|
|
7
|
-
export { listOAuthProvidersFromAi as listOAuthProviders };
|
|
8
|
-
export { getOAuthApiKey, loadOAuthCredentials, removeOAuthCredentials, resetOAuthStorage, saveOAuthCredentials, setOAuthStorage, };
|
|
9
|
-
export interface OAuthAuthInfo {
|
|
10
|
-
url: string;
|
|
11
|
-
instructions?: string;
|
|
12
|
-
}
|
|
13
|
-
export interface OAuthPrompt {
|
|
14
|
-
message: string;
|
|
15
|
-
placeholder?: string;
|
|
16
|
-
}
|
|
17
|
-
export type OAuthProviderInfo = {
|
|
18
|
-
id: OAuthProvider;
|
|
19
|
-
name: string;
|
|
20
|
-
description: string;
|
|
21
|
-
available: boolean;
|
|
22
|
-
};
|
|
23
|
-
export declare function getOAuthProviders(): OAuthProviderInfo[];
|
|
24
|
-
/**
|
|
25
|
-
* Login with OAuth provider
|
|
26
|
-
*/
|
|
27
|
-
export declare function login(provider: OAuthProvider, onAuth: (info: OAuthAuthInfo) => void, onPrompt: (prompt: OAuthPrompt) => Promise<string>, onProgress?: (message: string) => void): Promise<void>;
|
|
28
|
-
/**
|
|
29
|
-
* Logout from OAuth provider
|
|
30
|
-
*/
|
|
31
|
-
export declare function logout(provider: OAuthProvider): Promise<void>;
|
|
32
|
-
/**
|
|
33
|
-
* Refresh OAuth token for provider.
|
|
34
|
-
* Delegates to the ai package implementation.
|
|
35
|
-
*/
|
|
36
|
-
export declare function refreshToken(provider: OAuthProvider): Promise<string>;
|
|
37
|
-
/**
|
|
38
|
-
* Get OAuth token for provider (auto-refreshes if expired).
|
|
39
|
-
*/
|
|
40
|
-
export declare function getOAuthToken(provider: OAuthProvider): Promise<string | null>;
|
|
41
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/oauth/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACN,cAAc,EACd,kBAAkB,IAAI,wBAAwB,EAC9C,oBAAoB,EAKpB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EAExB,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,MAAM,qBAAqB,CAAC;AAG7B,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAC;AACrE,OAAO,EAAE,wBAAwB,IAAI,kBAAkB,EAAE,CAAC;AAC1D,OAAO,EACN,cAAc,EACd,oBAAoB,EACpB,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,GACf,CAAC;AAGF,MAAM,WAAW,aAAa;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC/B,EAAE,EAAE,aAAa,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,wBAAgB,iBAAiB,IAAI,iBAAiB,EAAE,CA2BvD;AAED;;GAEG;AACH,wBAAsB,KAAK,CAC1B,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,EACrC,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,EAClD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GACpC,OAAO,CAAC,IAAI,CAAC,CA4Bf;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnE;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAE3E;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnF","sourcesContent":["/**\n * OAuth management for coding-agent.\n * Re-exports from @mariozechner/pi-ai and adds convenience wrappers.\n */\n\nimport {\n\tgetOAuthApiKey,\n\tlistOAuthProviders as listOAuthProvidersFromAi,\n\tloadOAuthCredentials,\n\tloginAnthropic,\n\tloginAntigravity,\n\tloginGeminiCli,\n\tloginGitHubCopilot,\n\ttype OAuthCredentials,\n\ttype OAuthProvider,\n\ttype OAuthStorageBackend,\n\trefreshToken as refreshTokenFromAi,\n\tremoveOAuthCredentials,\n\tresetOAuthStorage,\n\tsaveOAuthCredentials,\n\tsetOAuthStorage,\n} from \"@mariozechner/pi-ai\";\n\n// Re-export types and functions\nexport type { OAuthCredentials, OAuthProvider, OAuthStorageBackend };\nexport { listOAuthProvidersFromAi as listOAuthProviders };\nexport {\n\tgetOAuthApiKey,\n\tloadOAuthCredentials,\n\tremoveOAuthCredentials,\n\tresetOAuthStorage,\n\tsaveOAuthCredentials,\n\tsetOAuthStorage,\n};\n\n// Types for OAuth flow\nexport interface OAuthAuthInfo {\n\turl: string;\n\tinstructions?: string;\n}\n\nexport interface OAuthPrompt {\n\tmessage: string;\n\tplaceholder?: string;\n}\n\nexport type OAuthProviderInfo = {\n\tid: OAuthProvider;\n\tname: string;\n\tdescription: string;\n\tavailable: boolean;\n};\n\nexport function getOAuthProviders(): OAuthProviderInfo[] {\n\treturn [\n\t\t{\n\t\t\tid: \"anthropic\",\n\t\t\tname: \"Anthropic (Claude Pro/Max)\",\n\t\t\tdescription: \"Use Claude with your Pro/Max subscription\",\n\t\t\tavailable: true,\n\t\t},\n\t\t{\n\t\t\tid: \"github-copilot\",\n\t\t\tname: \"GitHub Copilot\",\n\t\t\tdescription: \"Use models via GitHub Copilot subscription\",\n\t\t\tavailable: true,\n\t\t},\n\t\t{\n\t\t\tid: \"google-gemini-cli\",\n\t\t\tname: \"Google Gemini CLI\",\n\t\t\tdescription: \"Free Gemini 2.0/2.5 models via Google Cloud\",\n\t\t\tavailable: true,\n\t\t},\n\t\t{\n\t\t\tid: \"google-antigravity\",\n\t\t\tname: \"Antigravity\",\n\t\t\tdescription: \"Free Gemini 3, Claude, GPT-OSS via Google Cloud\",\n\t\t\tavailable: true,\n\t\t},\n\t];\n}\n\n/**\n * Login with OAuth provider\n */\nexport async function login(\n\tprovider: OAuthProvider,\n\tonAuth: (info: OAuthAuthInfo) => void,\n\tonPrompt: (prompt: OAuthPrompt) => Promise<string>,\n\tonProgress?: (message: string) => void,\n): Promise<void> {\n\tswitch (provider) {\n\t\tcase \"anthropic\":\n\t\t\tawait loginAnthropic(\n\t\t\t\t(url) => onAuth({ url }),\n\t\t\t\tasync () => onPrompt({ message: \"Paste the authorization code below:\" }),\n\t\t\t);\n\t\t\tbreak;\n\t\tcase \"github-copilot\": {\n\t\t\tconst creds = await loginGitHubCopilot({\n\t\t\t\tonAuth: (url, instructions) => onAuth({ url, instructions }),\n\t\t\t\tonPrompt,\n\t\t\t\tonProgress,\n\t\t\t});\n\t\t\tsaveOAuthCredentials(\"github-copilot\", creds);\n\t\t\tbreak;\n\t\t}\n\t\tcase \"google-gemini-cli\": {\n\t\t\tawait loginGeminiCli((info) => onAuth({ url: info.url, instructions: info.instructions }), onProgress);\n\t\t\tbreak;\n\t\t}\n\t\tcase \"google-antigravity\": {\n\t\t\tawait loginAntigravity((info) => onAuth({ url: info.url, instructions: info.instructions }), onProgress);\n\t\t\tbreak;\n\t\t}\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown OAuth provider: ${provider}`);\n\t}\n}\n\n/**\n * Logout from OAuth provider\n */\nexport async function logout(provider: OAuthProvider): Promise<void> {\n\tremoveOAuthCredentials(provider);\n}\n\n/**\n * Refresh OAuth token for provider.\n * Delegates to the ai package implementation.\n */\nexport async function refreshToken(provider: OAuthProvider): Promise<string> {\n\treturn refreshTokenFromAi(provider);\n}\n\n/**\n * Get OAuth token for provider (auto-refreshes if expired).\n */\nexport async function getOAuthToken(provider: OAuthProvider): Promise<string | null> {\n\treturn getOAuthApiKey(provider);\n}\n"]}
|