mini-coder 0.7.3 → 0.7.4

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/bun.lock CHANGED
@@ -7,7 +7,7 @@
7
7
  "dependencies": {
8
8
  "@cel-tui/components": "^0.8.3",
9
9
  "@cel-tui/core": "^0.8.3",
10
- "@earendil-works/pi-ai": "^0.79.3",
10
+ "@earendil-works/pi-ai": "^0.80.3",
11
11
  "diff": "^9.0.0",
12
12
  "simple-git": "^3.36.0",
13
13
  "yaml": "^2.9.0",
@@ -101,7 +101,7 @@
101
101
 
102
102
  "@cel-tui/types": ["@cel-tui/types@0.8.3", "", { "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-yEKXCY16U/W4A4JaVysv2/x/IBEHQAfb93CzL6HugN+N15ETB3Vn8tZrQwdyrOT/9WdlCQkeBH8Vj7BJHs4Hkw=="],
103
103
 
104
- "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.79.3", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.1", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-lMSput/haP5uZAGbXhS5rAYd3GB7GYdJkoAUxg3VFummBeqGqGqllaTWrbHFN12kVGyVfWHhdySNXkiqVh65Iw=="],
104
+ "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.80.3", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.6", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-jPZLMeGL5kkMSEAwAklfXTMHqZvfhsJtCCpKGIr5Duk7mc0n4skjB1dugk7y0z3z8ZHIUCmPAWHdyDqgUz5vdA=="],
105
105
 
106
106
  "@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="],
107
107
 
@@ -109,10 +109,14 @@
109
109
 
110
110
  "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="],
111
111
 
112
- "@mistralai/mistralai": ["@mistralai/mistralai@2.2.1", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" } }, "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ=="],
112
+ "@mistralai/mistralai": ["@mistralai/mistralai@2.2.6", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ=="],
113
113
 
114
114
  "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="],
115
115
 
116
+ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
117
+
118
+ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
119
+
116
120
  "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
117
121
 
118
122
  "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mini-coder",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "type": "module",
5
5
  "packageManager": "bun@1.3.13",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "@cel-tui/components": "^0.8.3",
24
24
  "@cel-tui/core": "^0.8.3",
25
- "@earendil-works/pi-ai": "^0.79.3",
25
+ "@earendil-works/pi-ai": "^0.80.3",
26
26
  "diff": "^9.0.0",
27
27
  "simple-git": "^3.36.0",
28
28
  "yaml": "^2.9.0"
package/src/agent.ts CHANGED
@@ -1,14 +1,13 @@
1
- import {
2
- type AssistantMessage,
3
- type Context,
4
- type ImageContent,
5
- type Message,
6
- streamSimple,
7
- type TextContent,
8
- type ToolCall,
9
- type ToolResultMessage,
1
+ import type {
2
+ AssistantMessage,
3
+ Context,
4
+ ImageContent,
5
+ Message,
6
+ TextContent,
7
+ ToolCall,
8
+ ToolResultMessage,
10
9
  } from "@earendil-works/pi-ai";
11
- import { getApiKey } from "./oauth";
10
+ import { createAppModels } from "./models.ts";
12
11
  import { estimateTokens } from "./shared";
13
12
  import type {
14
13
  AgentContex,
@@ -21,17 +20,31 @@ import type {
21
20
  // Published research found that **simply hiding old tool outputs** matched the quality of full LLM summarization with **zero extra compute**:
22
21
  // https://blog.jetbrains.com/research/2025/12/efficient-context-management/
23
22
  export function compactContext(messages: Message[]) {
24
- // TODO: Preserve SKILL.md contents, and exclude them from compaction.
25
- // Problem: How do we know? we need to check each message result againt it's arguments
26
- // and then find if by chance it has a read skill command... This is a mess. We we.
27
- // A better way would be to first add a read(path, lines, offset). And then we know
28
- // which files are read using it, and can match by path if it's a SKILL.md ending.
29
23
  const KEEP_OBSERVATIONS = 10;
30
24
 
25
+ const skillReadToolCallIds = new Set<string>();
26
+ for (const message of messages) {
27
+ if (message.role !== "assistant") continue;
28
+
29
+ for (const content of message.content) {
30
+ if (content.type !== "toolCall") continue;
31
+ if (content.name !== "read") continue;
32
+
33
+ const path = content.arguments.path;
34
+ if (typeof path === "string" && path.endsWith("SKILL.md")) {
35
+ skillReadToolCallIds.add(content.id);
36
+ }
37
+ }
38
+ }
39
+
31
40
  const toolResultIndices: number[] = [];
32
41
  for (let i = 0; i < messages.length; i++) {
33
42
  if (messages[i].role !== "toolResult") continue;
34
- const content = (messages[i] as ToolResultMessage).content;
43
+
44
+ const message = messages[i] as ToolResultMessage;
45
+ if (skillReadToolCallIds.has(message.toolCallId)) continue;
46
+
47
+ const content = message.content;
35
48
  if (
36
49
  content.length === 1 &&
37
50
  content[0].type === "text" &&
@@ -73,15 +86,17 @@ export async function* streamAgent(
73
86
  messages: agentCtx.messages,
74
87
  };
75
88
 
76
- // Important for refreshing tokens.
77
- const apiKey = await getApiKey(agentCtx.options);
89
+ const models = createAppModels(agentCtx.options.customProviders);
78
90
 
79
91
  // Main agent loop, continues until llm sends a response other than toolCall or has no tool calls.
80
92
  while (true) {
81
- const s = streamSimple(agentCtx.options.model, llmCtx, {
93
+ const estimate = estimateTokens(JSON.stringify(llmCtx));
94
+ // 80k is the agreed uppon threshold to the DUMB ZONE
95
+ if (estimate > 80000) compactContext(llmCtx.messages);
96
+
97
+ const s = models.streamSimple(agentCtx.options.model, llmCtx, {
82
98
  reasoning: agentCtx.options.effort,
83
99
  signal: agentCtx.signal,
84
- apiKey,
85
100
  });
86
101
 
87
102
  let partial: AssistantMessage | null = null;
@@ -116,6 +131,8 @@ export async function* streamAgent(
116
131
 
117
132
  case "error": {
118
133
  const finalMessage = await s.result();
134
+ if (!finalMessage) throw new Error(e.error.errorMessage);
135
+
119
136
  if (added) {
120
137
  llmCtx.messages[llmCtx.messages.length - 1] = finalMessage;
121
138
  } else {
@@ -127,15 +144,11 @@ export async function* streamAgent(
127
144
  return;
128
145
  }
129
146
  }
130
-
131
- // Experiment: use this inside the inner loop for "running" compaction
132
- // after we are in the dumb zone.
133
- const estimate = estimateTokens(JSON.stringify(llmCtx));
134
- // 80k is the agreed uppon threshold to the DUMB ZONE
135
- if (estimate > 80000) compactContext(llmCtx.messages);
136
147
  }
137
148
 
138
149
  const message = await s.result();
150
+ if (!message) throw new Error("Stream finished without a message");
151
+
139
152
  if (added) {
140
153
  llmCtx.messages[llmCtx.messages.length - 1] = message;
141
154
  } else {
@@ -144,7 +157,9 @@ export async function* streamAgent(
144
157
  }
145
158
  yield { type: "message_end", message };
146
159
 
147
- const toolCalls = message.content.filter((c) => c.type === "toolCall");
160
+ const toolCalls = message.content.filter(
161
+ (c): c is ToolCall => c.type === "toolCall",
162
+ );
148
163
 
149
164
  // Stop on errors or no tools to call.
150
165
  if (message.stopReason !== "toolUse" || toolCalls.length === 0) {
package/src/args.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  import { mkdir } from "node:fs/promises";
2
- import {
3
- type Api,
4
- getModels,
5
- getProviders,
6
- type Model,
7
- type ThinkingLevel,
8
- } from "@earendil-works/pi-ai";
2
+ import type { ThinkingLevel } from "@earendil-works/pi-ai";
9
3
  import { Value } from "typebox/value";
4
+ import {
5
+ findModelConfig,
6
+ getFallbackModel,
7
+ getFirstModelConfig,
8
+ } from "./models.ts";
10
9
  import { getAvailableProviders, isOAuthProvider, loginOAuth } from "./oauth";
11
10
  import { DATA_DIR, SETTINGS_PATH } from "./shared.ts";
12
11
  import { DEFAULT_TUI_THEME_ID } from "./themes.ts";
@@ -92,53 +91,6 @@ function requireValue(argv: string[], index: number, flag: string) {
92
91
  return value;
93
92
  }
94
93
 
95
- function findModelConfig(
96
- modelId: string,
97
- provider: string,
98
- customProviders?: Model<Api>[],
99
- ) {
100
- const knownProviders = getProviders() as string[];
101
- if (knownProviders.includes(provider)) {
102
- const models = getModels(provider as any);
103
- if (models.length === 0) throw new Error("Provider has no models");
104
- return models.find((m) => m.id === modelId);
105
- }
106
- return customProviders?.find(
107
- (m) => m.provider === provider && m.id === modelId,
108
- );
109
- }
110
-
111
- function getFirstModelConfig(
112
- provider: string,
113
- customProviders?: Model<Api>[],
114
- ): Model<Api> {
115
- const knownProviders = getProviders() as string[];
116
- if (knownProviders.includes(provider)) {
117
- const models = getModels(provider as any);
118
- if (models.length > 0) return models[0];
119
- }
120
- const custom = customProviders?.find((m) => m.provider === provider);
121
- if (custom) return custom;
122
- throw new Error("Provider has no models");
123
- }
124
-
125
- function getFallbackModel(
126
- provider: string,
127
- explicitModel: boolean,
128
- providerChanged: boolean,
129
- customProviders?: Model<Api>[],
130
- ): Model<Api> {
131
- if (explicitModel) {
132
- throw new Error("Model not found");
133
- }
134
-
135
- if (!providerChanged) {
136
- throw new Error("Model not found");
137
- }
138
-
139
- return getFirstModelConfig(provider, customProviders);
140
- }
141
-
142
94
  export async function handleArgv(argv: string[]): Promise<CliOptions> {
143
95
  const settings = await getSettings();
144
96
  const customProviders = settings?.customProviders ?? [];
package/src/models.ts ADDED
@@ -0,0 +1,191 @@
1
+ import type {
2
+ Api,
3
+ Credential,
4
+ CredentialStore,
5
+ KnownApi,
6
+ KnownProvider,
7
+ Model,
8
+ ProviderStreams,
9
+ } from "@earendil-works/pi-ai";
10
+ import { createProvider } from "@earendil-works/pi-ai";
11
+ import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
12
+ import { azureOpenAIResponsesApi } from "@earendil-works/pi-ai/api/azure-openai-responses.lazy";
13
+ import { bedrockConverseStreamApi } from "@earendil-works/pi-ai/api/bedrock-converse-stream.lazy";
14
+ import { googleGenerativeAIApi } from "@earendil-works/pi-ai/api/google-generative-ai.lazy";
15
+ import { googleVertexApi } from "@earendil-works/pi-ai/api/google-vertex.lazy";
16
+ import { mistralConversationsApi } from "@earendil-works/pi-ai/api/mistral-conversations.lazy";
17
+ import { openAICodexResponsesApi } from "@earendil-works/pi-ai/api/openai-codex-responses.lazy";
18
+ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
19
+ import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
20
+ import {
21
+ builtinModels,
22
+ getBuiltinModels,
23
+ getBuiltinProviders,
24
+ } from "@earendil-works/pi-ai/providers/all";
25
+ import { AUTH_PATH } from "./shared.ts";
26
+
27
+ const API_STREAMS: Record<KnownApi, () => ProviderStreams> = {
28
+ "anthropic-messages": anthropicMessagesApi,
29
+ "azure-openai-responses": azureOpenAIResponsesApi,
30
+ "bedrock-converse-stream": bedrockConverseStreamApi,
31
+ "google-generative-ai": googleGenerativeAIApi,
32
+ "google-vertex": googleVertexApi,
33
+ "mistral-conversations": mistralConversationsApi,
34
+ "openai-codex-responses": openAICodexResponsesApi,
35
+ "openai-completions": openAICompletionsApi,
36
+ "openai-responses": openAIResponsesApi,
37
+ };
38
+
39
+ type SavedCredentials = Record<string, Credential>;
40
+
41
+ async function readCredentials(): Promise<SavedCredentials> {
42
+ const file = Bun.file(AUTH_PATH);
43
+ if (await file.exists()) {
44
+ return JSON.parse(await file.text()) as SavedCredentials;
45
+ }
46
+
47
+ return {};
48
+ }
49
+
50
+ async function writeCredentials(credentials: SavedCredentials) {
51
+ await Bun.write(AUTH_PATH, JSON.stringify(credentials));
52
+ }
53
+
54
+ export function createCredentialStore(): CredentialStore {
55
+ return {
56
+ async read(providerId) {
57
+ const credentials = await readCredentials();
58
+ return credentials[providerId];
59
+ },
60
+
61
+ async modify(providerId, fn) {
62
+ const credentials = await readCredentials();
63
+ const next = await fn(credentials[providerId]);
64
+
65
+ if (next) {
66
+ credentials[providerId] = next;
67
+ await writeCredentials(credentials);
68
+ }
69
+
70
+ return credentials[providerId];
71
+ },
72
+
73
+ async delete(providerId) {
74
+ const credentials = await readCredentials();
75
+ delete credentials[providerId];
76
+ await writeCredentials(credentials);
77
+ },
78
+ };
79
+ }
80
+
81
+ export function isBuiltinProvider(provider: string): provider is KnownProvider {
82
+ return getBuiltinProviders().includes(provider as KnownProvider);
83
+ }
84
+
85
+ export function findModelConfig(
86
+ modelId: string,
87
+ provider: string,
88
+ customProviders?: Model<Api>[],
89
+ ) {
90
+ if (isBuiltinProvider(provider)) {
91
+ const models = getBuiltinModels(provider);
92
+ if (models.length === 0) throw new Error("Provider has no models");
93
+ return models.find((m) => m.id === modelId);
94
+ }
95
+ return customProviders?.find(
96
+ (m) => m.provider === provider && m.id === modelId,
97
+ );
98
+ }
99
+
100
+ export function getFirstModelConfig(
101
+ provider: string,
102
+ customProviders?: Model<Api>[],
103
+ ): Model<Api> {
104
+ if (isBuiltinProvider(provider)) {
105
+ const models = getBuiltinModels(provider);
106
+ if (models.length > 0) return models[0];
107
+ }
108
+ const custom = customProviders?.find((m) => m.provider === provider);
109
+ if (custom) return custom;
110
+ throw new Error("Provider has no models");
111
+ }
112
+
113
+ export function getFallbackModel(
114
+ provider: string,
115
+ explicitModel: boolean,
116
+ providerChanged: boolean,
117
+ customProviders?: Model<Api>[],
118
+ ): Model<Api> {
119
+ if (explicitModel) {
120
+ throw new Error("Model not found");
121
+ }
122
+
123
+ if (!providerChanged) {
124
+ throw new Error("Model not found");
125
+ }
126
+
127
+ return getFirstModelConfig(provider, customProviders);
128
+ }
129
+
130
+ export function getProviderModels(
131
+ provider: string,
132
+ customProviders?: Model<Api>[],
133
+ ): Model<Api>[] {
134
+ const builtIn = isBuiltinProvider(provider) ? getBuiltinModels(provider) : [];
135
+ const custom = customProviders?.filter((m) => m.provider === provider) ?? [];
136
+
137
+ return [...builtIn, ...custom];
138
+ }
139
+
140
+ export function createAppModels(customProviders?: Model<Api>[]) {
141
+ const models = builtinModels({ credentials: createCredentialStore() });
142
+ const customProviderGroups = new Map<string, Model<Api>[]>();
143
+
144
+ for (const model of customProviders ?? []) {
145
+ const group = customProviderGroups.get(model.provider) ?? [];
146
+ group.push(model);
147
+ customProviderGroups.set(model.provider, group);
148
+ }
149
+
150
+ for (const [provider, providerModels] of customProviderGroups) {
151
+ models.setProvider(
152
+ createProvider({
153
+ id: provider,
154
+ name: provider,
155
+ models: providerModels,
156
+ auth: {
157
+ apiKey: {
158
+ name: provider,
159
+ resolve: async () => ({ auth: { apiKey: "dummy" } }),
160
+ },
161
+ },
162
+ api: Object.fromEntries(
163
+ Object.entries(API_STREAMS)
164
+ .filter(([api]) =>
165
+ providerModels.some((model) => model.api === api),
166
+ )
167
+ .map(([api, createStreams]) => [api, createStreams()]),
168
+ ),
169
+ }),
170
+ );
171
+ }
172
+
173
+ return models;
174
+ }
175
+
176
+ export async function getConfiguredBuiltinProviders(): Promise<string[]> {
177
+ const models = builtinModels({ credentials: createCredentialStore() });
178
+ const providers: string[] = [];
179
+
180
+ for (const provider of models.getProviders()) {
181
+ const model = provider.getModels()[0];
182
+ if (!model) continue;
183
+
184
+ try {
185
+ const auth = await models.getAuth(model);
186
+ if (auth) providers.push(provider.id);
187
+ } catch {}
188
+ }
189
+
190
+ return providers;
191
+ }
package/src/oauth.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
  import readline from "node:readline";
3
3
 
4
- import { getEnvApiKey, getProviders } from "@earendil-works/pi-ai";
5
4
  import {
6
- getOAuthApiKey,
7
5
  getOAuthProvider,
8
6
  getOAuthProviders,
9
7
  type OAuthLoginCallbacks,
@@ -11,6 +9,11 @@ import {
11
9
  type OAuthProviderId,
12
10
  type OAuthSelectPrompt,
13
11
  } from "@earendil-works/pi-ai/oauth";
12
+ import {
13
+ createAppModels,
14
+ getConfiguredBuiltinProviders,
15
+ isBuiltinProvider,
16
+ } from "./models.ts";
14
17
  import { AUTH_PATH as AUTH_FILE, DATA_DIR } from "./shared";
15
18
  import type { CliOptions, SavedOAuthCreds } from "./types";
16
19
 
@@ -66,9 +69,7 @@ export async function getAvailableProviders(): Promise<string[]> {
66
69
  const loggedInOAuthProviders = getOAuthProviders()
67
70
  .map((provider) => provider.id)
68
71
  .filter((provider) => auth[provider]);
69
- const envKeyProviders = getProviders().filter(
70
- (provider) => !!getEnvApiKey(provider),
71
- );
72
+ const envKeyProviders = await getConfiguredBuiltinProviders();
72
73
  const providers: string[] = [];
73
74
  const providerIds = new Set<string>();
74
75
 
@@ -104,23 +105,12 @@ export async function loginOAuth(provider: OAuthProviderId) {
104
105
 
105
106
  export async function getApiKey(options: CliOptions) {
106
107
  const provider = options.model.provider;
107
- const auth = await readCreds();
108
-
109
- if (isOAuthProvider(provider)) {
110
- const result = await getOAuthApiKey(provider, auth);
111
- if (result) {
112
- auth[provider] = { type: "oauth", ...result.newCredentials };
113
- await writeCreds(auth);
114
-
115
- return result.apiKey;
116
- }
117
- }
108
+ const models = createAppModels(options.customProviders);
109
+ const auth = await models.getAuth(options.model);
118
110
 
119
- const envApiKey = getEnvApiKey(provider);
120
- if (envApiKey) return envApiKey;
111
+ if (auth?.auth.apiKey) return auth.auth.apiKey;
121
112
 
122
- const knownProviders = getProviders() as string[];
123
- if (!knownProviders.includes(provider)) {
113
+ if (!isBuiltinProvider(provider)) {
124
114
  if (options.model.api === "openai-completions") {
125
115
  // pi-ai requires a truthy apiKey for OpenAI-compatible local providers like Ollama.
126
116
  return "dummy";
@@ -1,11 +1,8 @@
1
1
  import { cel, HStack, Text, TextInput, VStack } from "@cel-tui/core";
2
- import {
3
- getModels,
4
- type Message,
5
- type ThinkingLevel,
6
- } from "@earendil-works/pi-ai";
2
+ import type { Message, ThinkingLevel } from "@earendil-works/pi-ai";
7
3
  import { getOAuthProviders } from "@earendil-works/pi-ai/oauth";
8
4
  import { saveSettings } from "./args";
5
+ import { findModelConfig, getProviderModels } from "./models.ts";
9
6
  import { getAvailableProviders } from "./oauth";
10
7
  import { listSessionsForCwd } from "./session";
11
8
  import { estimateTokens, formatTimestamp } from "./shared";
@@ -291,20 +288,11 @@ export function mainMenu(state: TUIState, initialPane = "main") {
291
288
  "xhigh",
292
289
  ];
293
290
  const efforts = reasoningEfforts.map((v) => ({ label: v, value: v }));
294
- const getProviderModels = (provider: string) => {
295
- const builtIn = getModels(provider as any).map((v) => ({
291
+ const getProviderModelOptions = (provider: string) =>
292
+ getProviderModels(provider, state.options.customProviders).map((v) => ({
296
293
  label: v.name,
297
294
  value: v.id,
298
295
  }));
299
- const custom =
300
- state.options.customProviders
301
- ?.filter((m) => m.provider === provider)
302
- .map((v) => ({
303
- label: v.name,
304
- value: v.id,
305
- })) ?? [];
306
- return [...builtIn, ...custom];
307
- };
308
296
 
309
297
  let currentProviders: string[] = [];
310
298
  let currentSessions: Session[] = [];
@@ -560,7 +548,7 @@ export function mainMenu(state: TUIState, initialPane = "main") {
560
548
  openPane(s, {
561
549
  label: "models",
562
550
  filter: "",
563
- list: getProviderModels(selectedProvider),
551
+ list: getProviderModelOptions(selectedProvider),
564
552
  });
565
553
  return;
566
554
  }
@@ -568,13 +556,11 @@ export function mainMenu(state: TUIState, initialPane = "main") {
568
556
  if (currentPane.label === "models") {
569
557
  if (!selectedProvider) return;
570
558
 
571
- const builtIn = getModels(selectedProvider as any).find(
572
- (v) => v.id === s.selected,
573
- );
574
- const custom = state.options.customProviders?.find(
575
- (v) => v.provider === selectedProvider && v.id === s.selected,
559
+ const model = findModelConfig(
560
+ s.selected,
561
+ selectedProvider,
562
+ state.options.customProviders,
576
563
  );
577
- const model = builtIn ?? custom;
578
564
  if (!model) return;
579
565
 
580
566
  state.options.provider = selectedProvider;