pi-free 2.2.3 → 2.2.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/CHANGELOG.md +16 -49
- package/README.md +41 -532
- package/banner.svg +23 -20
- package/config.ts +82 -10
- package/constants.ts +11 -1
- package/index.ts +15 -1
- package/lib/model-detection.ts +296 -296
- package/lib/model-metadata.ts +10 -3
- package/lib/telemetry.ts +36 -44
- package/package.json +3 -2
- package/provider-failover/benchmark-lookup.ts +30 -15
- package/provider-helper.ts +27 -8
- package/providers/bai/bai.ts +2 -7
- package/providers/cline/cline-xml-bridge.ts +31 -25
- package/providers/cline/cline.ts +17 -8
- package/providers/kilo/kilo.ts +11 -6
- package/providers/model-fetcher.ts +1 -1
- package/providers/opencode-session.ts +2 -2
- package/providers/openmodel/openmodel.ts +525 -0
- package/providers/qoder/auth.ts +548 -0
- package/providers/qoder/cosy.ts +236 -0
- package/providers/qoder/encoding.ts +48 -0
- package/providers/qoder/models.ts +321 -0
- package/providers/qoder/qoder.ts +154 -0
- package/providers/qoder/stream.ts +677 -0
- package/providers/qoder/thinking-parser.ts +251 -0
- package/providers/qoder/transform.ts +189 -0
- package/providers/tokenrouter/tokenrouter.ts +3 -6
package/lib/telemetry.ts
CHANGED
|
@@ -105,67 +105,59 @@ const _store = createJSONStore<TelemetryStore>(TELEMETRY_FILE, {
|
|
|
105
105
|
// =============================================================================
|
|
106
106
|
|
|
107
107
|
function deriveModelTelemetry(
|
|
108
|
-
_modelKey: string,
|
|
109
108
|
entries: TelemetryEntry[],
|
|
110
109
|
): ModelTelemetry {
|
|
111
110
|
const recent = entries.slice(-MAX_RECENT_CALLS);
|
|
111
|
+
|
|
112
|
+
let successCalls = 0;
|
|
113
|
+
let totalTokensFromSuccessful = 0;
|
|
114
|
+
let totalLatencyFromSuccessful = 0;
|
|
115
|
+
let totalTokens = 0;
|
|
116
|
+
let totalPromptTokens = 0;
|
|
117
|
+
let totalCompletionTokens = 0;
|
|
118
|
+
let totalLatencyMs = 0;
|
|
119
|
+
let totalCost = 0;
|
|
120
|
+
|
|
121
|
+
for (const e of entries) {
|
|
122
|
+
totalTokens += e.totalTokens;
|
|
123
|
+
totalPromptTokens += e.promptTokens;
|
|
124
|
+
totalCompletionTokens += e.completionTokens;
|
|
125
|
+
totalLatencyMs += e.latencyMs;
|
|
126
|
+
totalCost += e.cost;
|
|
127
|
+
if (e.success) {
|
|
128
|
+
successCalls++;
|
|
129
|
+
totalTokensFromSuccessful += e.totalTokens;
|
|
130
|
+
totalLatencyFromSuccessful += e.latencyMs;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
const totalCalls = entries.length;
|
|
113
|
-
const successCalls = entries.filter((e) => e.success).length;
|
|
114
|
-
const errorCalls = totalCalls - successCalls;
|
|
115
|
-
|
|
116
|
-
const stats = entries.reduce(
|
|
117
|
-
(acc, e) => {
|
|
118
|
-
acc.totalTokens += e.totalTokens;
|
|
119
|
-
acc.totalPromptTokens += e.promptTokens;
|
|
120
|
-
acc.totalCompletionTokens += e.completionTokens;
|
|
121
|
-
acc.totalLatencyMs += e.latencyMs;
|
|
122
|
-
acc.totalCost += e.cost;
|
|
123
|
-
return acc;
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
totalTokens: 0,
|
|
127
|
-
totalPromptTokens: 0,
|
|
128
|
-
totalCompletionTokens: 0,
|
|
129
|
-
totalLatencyMs: 0,
|
|
130
|
-
totalCost: 0,
|
|
131
|
-
},
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
const totalSuccessEntries = entries.filter((e) => e.success);
|
|
135
|
-
const totalTokensFromSuccessful = totalSuccessEntries.reduce(
|
|
136
|
-
(s, e) => s + e.totalTokens,
|
|
137
|
-
0,
|
|
138
|
-
);
|
|
139
|
-
const totalLatencyFromSuccessful = totalSuccessEntries.reduce(
|
|
140
|
-
(s, e) => s + e.latencyMs,
|
|
141
|
-
0,
|
|
142
|
-
);
|
|
143
135
|
|
|
144
136
|
return {
|
|
145
137
|
totalCalls,
|
|
146
138
|
successCalls,
|
|
147
|
-
errorCalls,
|
|
148
|
-
totalTokens
|
|
149
|
-
totalPromptTokens
|
|
150
|
-
totalCompletionTokens
|
|
151
|
-
totalLatencyMs
|
|
152
|
-
totalCost
|
|
139
|
+
errorCalls: totalCalls - successCalls,
|
|
140
|
+
totalTokens,
|
|
141
|
+
totalPromptTokens,
|
|
142
|
+
totalCompletionTokens,
|
|
143
|
+
totalLatencyMs,
|
|
144
|
+
totalCost,
|
|
153
145
|
avgLatencyMs:
|
|
154
|
-
|
|
155
|
-
? Math.round(totalLatencyFromSuccessful /
|
|
146
|
+
successCalls > 0
|
|
147
|
+
? Math.round(totalLatencyFromSuccessful / successCalls)
|
|
156
148
|
: 0,
|
|
157
149
|
avgTokensPerSecond:
|
|
158
150
|
totalLatencyFromSuccessful > 0
|
|
159
|
-
? parseFloat(
|
|
151
|
+
? Number.parseFloat(
|
|
160
152
|
(
|
|
161
153
|
totalTokensFromSuccessful /
|
|
162
154
|
(totalLatencyFromSuccessful / 1000)
|
|
163
155
|
).toFixed(1),
|
|
164
|
-
|
|
156
|
+
)
|
|
165
157
|
: 0,
|
|
166
158
|
successRate:
|
|
167
159
|
totalCalls > 0
|
|
168
|
-
? parseFloat(((successCalls / totalCalls) * 100).toFixed(1))
|
|
160
|
+
? Number.parseFloat(((successCalls / totalCalls) * 100).toFixed(1))
|
|
169
161
|
: 0,
|
|
170
162
|
recentCalls: recent,
|
|
171
163
|
};
|
|
@@ -186,7 +178,7 @@ async function addEntry(entry: TelemetryEntry): Promise<void> {
|
|
|
186
178
|
...store,
|
|
187
179
|
models: {
|
|
188
180
|
...store.models,
|
|
189
|
-
[modelKey]: deriveModelTelemetry(
|
|
181
|
+
[modelKey]: deriveModelTelemetry(pruned),
|
|
190
182
|
},
|
|
191
183
|
lastUpdated: Date.now(),
|
|
192
184
|
};
|
|
@@ -310,7 +302,7 @@ export async function recordModelCall(
|
|
|
310
302
|
const totalTokens = usage.totalTokens || usage.input + usage.output;
|
|
311
303
|
const tokensPerSecond =
|
|
312
304
|
latencyMs > 0
|
|
313
|
-
? parseFloat((totalTokens / (latencyMs / 1000)).toFixed(1))
|
|
305
|
+
? Number.parseFloat((totalTokens / (latencyMs / 1000)).toFixed(1))
|
|
314
306
|
: 0;
|
|
315
307
|
|
|
316
308
|
const entry: TelemetryEntry = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-free",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI model providers for Pi with free model filtering and dynamic model fetching",
|
|
6
6
|
"keywords": [
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"test": "vitest",
|
|
53
53
|
"test:ui": "vitest --ui",
|
|
54
54
|
"test:run": "vitest run",
|
|
55
|
-
"smoke:cline": "tsx scripts/smoke-cline-xml-bridge.ts"
|
|
55
|
+
"smoke:cline": "tsx scripts/smoke-cline-xml-bridge.ts",
|
|
56
|
+
"smoke:openmodel": "tsx scripts/smoke-openmodel-wire-format.ts"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
59
|
"@earendil-works/pi-ai": "^0.79.8",
|
|
@@ -551,23 +551,35 @@ function tryDirectSubstringMatch(
|
|
|
551
551
|
modelId: string,
|
|
552
552
|
modelName: string,
|
|
553
553
|
): HardcodedBenchmark | null {
|
|
554
|
+
// Collect ALL substring matches, then return the LONGEST key. This
|
|
555
|
+
// prevents short general keys (e.g. "mistral-medium-3") from shadowing
|
|
556
|
+
// longer specific keys (e.g. "mistral-medium-3.5") when a provider
|
|
557
|
+
// uses a different separator convention in the model ID.
|
|
558
|
+
let bestKey: string | null = null;
|
|
559
|
+
let bestData: HardcodedBenchmark | null = null;
|
|
554
560
|
for (const [key, data] of Object.entries(HARDCODED_BENCHMARKS) as [
|
|
555
561
|
string,
|
|
556
562
|
HardcodedBenchmark,
|
|
557
563
|
][]) {
|
|
558
564
|
if (search.includes(key.toLowerCase())) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
action: "match",
|
|
564
|
-
strategy: "direct-substring",
|
|
565
|
-
matchKey: key,
|
|
566
|
-
codingIndex: data.codingIndex,
|
|
567
|
-
});
|
|
568
|
-
return data;
|
|
565
|
+
if (bestKey === null || key.length > bestKey.length) {
|
|
566
|
+
bestKey = key;
|
|
567
|
+
bestData = data;
|
|
568
|
+
}
|
|
569
569
|
}
|
|
570
570
|
}
|
|
571
|
+
if (bestKey !== null && bestData !== null) {
|
|
572
|
+
logDebug({
|
|
573
|
+
provider,
|
|
574
|
+
modelId,
|
|
575
|
+
modelName,
|
|
576
|
+
action: "match",
|
|
577
|
+
strategy: "direct-substring",
|
|
578
|
+
matchKey: bestKey,
|
|
579
|
+
codingIndex: bestData.codingIndex,
|
|
580
|
+
});
|
|
581
|
+
return bestData;
|
|
582
|
+
}
|
|
571
583
|
return null;
|
|
572
584
|
}
|
|
573
585
|
|
|
@@ -685,14 +697,17 @@ export function findHardcodedBenchmark(
|
|
|
685
697
|
|
|
686
698
|
logDebug({ provider, modelId, modelName, action: "attempt" });
|
|
687
699
|
|
|
688
|
-
// 1.
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
// 2. Variant alias matching
|
|
700
|
+
// 1. Variant alias matching (human-curated, runs first so deliberate
|
|
701
|
+
// aliases for separator/suffix mismatches can override generic substring
|
|
702
|
+
// matches).
|
|
693
703
|
const variant = tryVariantAliasMatch(search, provider, modelId, modelName);
|
|
694
704
|
if (variant) return variant;
|
|
695
705
|
|
|
706
|
+
// 2. Direct substring match (longest-key wins, so "minimax-m2.5" beats
|
|
707
|
+
// "minimax-m2" when both could match).
|
|
708
|
+
const direct = tryDirectSubstringMatch(search, provider, modelId, modelName);
|
|
709
|
+
if (direct) return direct;
|
|
710
|
+
|
|
696
711
|
// 3. Provider-specific normalization
|
|
697
712
|
const { result: normalizedResult, normalized } = tryProviderNormalizedMatch(
|
|
698
713
|
modelId,
|
package/provider-helper.ts
CHANGED
|
@@ -13,14 +13,11 @@ import type {
|
|
|
13
13
|
} from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { saveConfig } from "./config.ts";
|
|
15
15
|
import { createLogger } from "./lib/logger.ts";
|
|
16
|
+
import type { ModelsDevEnrichedMetadata } from "./lib/types.ts";
|
|
16
17
|
import { enhanceModelNameWithCodingIndex } from "./provider-failover/benchmark-lookup.ts";
|
|
17
18
|
|
|
18
19
|
const _logger = createLogger("provider-helper");
|
|
19
20
|
|
|
20
|
-
type ModelsDevEnrichedMetadata = {
|
|
21
|
-
modelsDev?: Parameters<typeof enhanceModelNameWithCodingIndex>[3];
|
|
22
|
-
};
|
|
23
|
-
|
|
24
21
|
// =============================================================================
|
|
25
22
|
// Types
|
|
26
23
|
// =============================================================================
|
|
@@ -66,6 +63,14 @@ export interface OpenAICompatibleConfig {
|
|
|
66
63
|
baseUrl: string;
|
|
67
64
|
/** Environment variable name for the API key */
|
|
68
65
|
apiKey: string;
|
|
66
|
+
/**
|
|
67
|
+
* Wire API to use. Defaults to `"openai-completions"` for backward
|
|
68
|
+
* compatibility with the 17 existing providers that pass through
|
|
69
|
+
* this helper without setting it. Set to `"anthropic-messages"`
|
|
70
|
+
* for Anthropic-protocol gateways (e.g. OpenModel). The pi-ai
|
|
71
|
+
* runtime dispatches to the right client based on this value.
|
|
72
|
+
*/
|
|
73
|
+
api?: "openai-completions" | "anthropic-messages";
|
|
69
74
|
/** Additional headers to include */
|
|
70
75
|
headers?: Record<string, string>;
|
|
71
76
|
/** OAuth configuration (optional) */
|
|
@@ -105,12 +110,19 @@ export function registerOpenAICompatible(
|
|
|
105
110
|
config: OpenAICompatibleConfig,
|
|
106
111
|
models: ProviderModelConfig[],
|
|
107
112
|
): void {
|
|
108
|
-
const {
|
|
113
|
+
const {
|
|
114
|
+
providerId,
|
|
115
|
+
baseUrl,
|
|
116
|
+
apiKey,
|
|
117
|
+
api = "openai-completions",
|
|
118
|
+
headers,
|
|
119
|
+
oauth,
|
|
120
|
+
} = config;
|
|
109
121
|
|
|
110
122
|
pi.registerProvider(providerId, {
|
|
111
123
|
baseUrl,
|
|
112
124
|
apiKey,
|
|
113
|
-
api
|
|
125
|
+
api,
|
|
114
126
|
headers: {
|
|
115
127
|
"User-Agent": "pi-free-providers",
|
|
116
128
|
...headers,
|
|
@@ -143,13 +155,20 @@ export function createCtxReRegister(
|
|
|
143
155
|
},
|
|
144
156
|
config: OpenAICompatibleConfig,
|
|
145
157
|
): (models: ProviderModelConfig[]) => void {
|
|
146
|
-
const {
|
|
158
|
+
const {
|
|
159
|
+
providerId,
|
|
160
|
+
baseUrl,
|
|
161
|
+
apiKey,
|
|
162
|
+
api = "openai-completions",
|
|
163
|
+
headers,
|
|
164
|
+
oauth,
|
|
165
|
+
} = config;
|
|
147
166
|
|
|
148
167
|
return (models: ProviderModelConfig[]) => {
|
|
149
168
|
ctx.modelRegistry.registerProvider(providerId, {
|
|
150
169
|
baseUrl,
|
|
151
170
|
apiKey,
|
|
152
|
-
api
|
|
171
|
+
api,
|
|
153
172
|
headers: {
|
|
154
173
|
"User-Agent": "pi-free-providers",
|
|
155
174
|
...headers,
|
package/providers/bai/bai.ts
CHANGED
|
@@ -47,16 +47,11 @@ const _logger = createLogger("bai");
|
|
|
47
47
|
// =============================================================================
|
|
48
48
|
// Known Free Models
|
|
49
49
|
// B.AI doesn't expose pricing via /v1/models, so known-free models are
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// suffixed IDs (catches dynamic promotional additions).
|
|
50
|
+
// detected by name suffix. Catches `:free`-tagged models the gateway
|
|
51
|
+
// advertises as promotional.
|
|
53
52
|
// =============================================================================
|
|
54
53
|
|
|
55
|
-
const BAI_KNOWN_FREE_MODELS = new Set(["minimax-m3", "MiniMax-M3"]);
|
|
56
|
-
|
|
57
54
|
function isBaiKnownFree(modelId: string): boolean {
|
|
58
|
-
if (BAI_KNOWN_FREE_MODELS.has(modelId)) return true;
|
|
59
|
-
// Catch any future `:free` suffixed model the gateway advertises
|
|
60
55
|
return modelId.toLowerCase().endsWith(":free");
|
|
61
56
|
}
|
|
62
57
|
|
|
@@ -170,7 +170,9 @@ const CORE_CLINE_TOOL_NAMES = [
|
|
|
170
170
|
|
|
171
171
|
function stringArg(args: Record<string, unknown>, key: string): string {
|
|
172
172
|
const value = args[key];
|
|
173
|
-
|
|
173
|
+
if (typeof value === "string") return value;
|
|
174
|
+
if (value == null) return "";
|
|
175
|
+
return String(value);
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
function booleanArg(args: Record<string, unknown>, key: string): boolean {
|
|
@@ -178,7 +180,8 @@ function booleanArg(args: Record<string, unknown>, key: string): boolean {
|
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
function shellQuote(value: string): string {
|
|
181
|
-
|
|
183
|
+
const escaped = value.replaceAll("'", `'"'"'`);
|
|
184
|
+
return `'${escaped}'`;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
function buildListFilesCommand(args: Record<string, unknown>): string {
|
|
@@ -639,23 +642,25 @@ function buildToolInstructions(tools: Tool[] | undefined): string {
|
|
|
639
642
|
if (bridges.length === 0) return "";
|
|
640
643
|
|
|
641
644
|
const sections = bridges.map((bridge) => {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
645
|
+
let params: string;
|
|
646
|
+
if (bridge.remoteName === "replace_in_file") {
|
|
647
|
+
params = [
|
|
648
|
+
" <path>path/to/file</path>",
|
|
649
|
+
" <diff>",
|
|
650
|
+
"------- SEARCH",
|
|
651
|
+
"exact text to replace",
|
|
652
|
+
"=======",
|
|
653
|
+
"new text",
|
|
654
|
+
"+++++++ REPLACE",
|
|
655
|
+
" </diff>",
|
|
656
|
+
].join("\n");
|
|
657
|
+
} else if (bridge.parameters.length) {
|
|
658
|
+
params = bridge.parameters
|
|
659
|
+
.map((name) => ` <${name}>value</${name}>`)
|
|
660
|
+
.join("\n");
|
|
661
|
+
} else {
|
|
662
|
+
params = " <arguments>{}</arguments>";
|
|
663
|
+
}
|
|
659
664
|
return [
|
|
660
665
|
`Tool: ${bridge.remoteName}`,
|
|
661
666
|
`Description: ${bridge.description ?? bridge.runtimeName}`,
|
|
@@ -1461,12 +1466,13 @@ export function streamClineXml(
|
|
|
1461
1466
|
pushToolCall(assistant, toolCall, stream);
|
|
1462
1467
|
}
|
|
1463
1468
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1469
|
+
if (toolCalls.length > 0) {
|
|
1470
|
+
assistant.stopReason = "toolUse";
|
|
1471
|
+
} else if (finishReason === "length") {
|
|
1472
|
+
assistant.stopReason = "length";
|
|
1473
|
+
} else {
|
|
1474
|
+
assistant.stopReason = "stop";
|
|
1475
|
+
}
|
|
1470
1476
|
stream.push({
|
|
1471
1477
|
type: "done",
|
|
1472
1478
|
reason: assistant.stopReason as "stop" | "length" | "toolUse",
|
package/providers/cline/cline.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
ExtensionAPI,
|
|
20
20
|
ProviderModelConfig,
|
|
21
21
|
} from "@earendil-works/pi-coding-agent";
|
|
22
|
-
import { getClineShowPaid } from "../../config.ts";
|
|
22
|
+
import { getClineApiKey, getClineShowPaid } from "../../config.ts";
|
|
23
23
|
import { BASE_URL_CLINE, PROVIDER_CLINE } from "../../constants.ts";
|
|
24
24
|
import {
|
|
25
25
|
DEFAULT_PROVIDER_CACHE_TTL_MS,
|
|
@@ -84,6 +84,9 @@ function toApiKey(credentials: OAuthCredentials): string {
|
|
|
84
84
|
// =============================================================================
|
|
85
85
|
|
|
86
86
|
export default async function clineProvider(pi: ExtensionAPI) {
|
|
87
|
+
const clineApiKey = getClineApiKey();
|
|
88
|
+
const useApiKeyAuth = !!clineApiKey;
|
|
89
|
+
|
|
87
90
|
let allModels: ProviderModelConfig[];
|
|
88
91
|
const cachedModels = loadProviderCache(PROVIDER_CLINE);
|
|
89
92
|
if (cachedModels && cachedModels.length > 0) {
|
|
@@ -114,16 +117,21 @@ export default async function clineProvider(pi: ExtensionAPI) {
|
|
|
114
117
|
baseUrl: BASE_URL_CLINE,
|
|
115
118
|
api: "cline-xml-tools" as const,
|
|
116
119
|
authHeader: false,
|
|
120
|
+
apiKey: clineApiKey,
|
|
117
121
|
headers: buildClineHeaders(),
|
|
118
122
|
streamSimple: (model, context, options) =>
|
|
119
123
|
streamClineXml(model as any, context, options, buildClineHeaders()),
|
|
120
124
|
models: enhanceWithCI(m),
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
...(useApiKeyAuth
|
|
126
|
+
? {}
|
|
127
|
+
: {
|
|
128
|
+
oauth: {
|
|
129
|
+
name: "Cline",
|
|
130
|
+
login: loginCline,
|
|
131
|
+
refreshToken: refreshClineToken,
|
|
132
|
+
getApiKey: toApiKey,
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
127
135
|
});
|
|
128
136
|
};
|
|
129
137
|
|
|
@@ -138,7 +146,8 @@ export default async function clineProvider(pi: ExtensionAPI) {
|
|
|
138
146
|
toggleState.applyCurrent(reRegister);
|
|
139
147
|
};
|
|
140
148
|
|
|
141
|
-
|
|
149
|
+
// Register with global toggle system (hasKey=true if API key auth configured)
|
|
150
|
+
registerWithGlobalToggle(PROVIDER_CLINE, stored, (m) => reRegister(m), useApiKeyAuth);
|
|
142
151
|
toggleState.applyCurrent(reRegister);
|
|
143
152
|
|
|
144
153
|
pi.registerCommand("toggle-cline", {
|
package/providers/kilo/kilo.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
ProviderModelConfig,
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import {
|
|
21
|
+
getKiloApiKey,
|
|
21
22
|
getKiloFreeOnly,
|
|
22
23
|
getKiloShowPaid,
|
|
23
24
|
PROVIDER_KILO,
|
|
@@ -152,7 +153,7 @@ function parseXmlToolCalls(
|
|
|
152
153
|
const KILO_PROVIDER_CONFIG = {
|
|
153
154
|
providerId: PROVIDER_KILO,
|
|
154
155
|
baseUrl: KILO_GATEWAY_BASE,
|
|
155
|
-
apiKey: "$KILO_API_KEY",
|
|
156
|
+
apiKey: getKiloApiKey() || "$KILO_API_KEY",
|
|
156
157
|
headers: {
|
|
157
158
|
"X-KILOCODE-EDITORNAME": "Pi",
|
|
158
159
|
},
|
|
@@ -172,14 +173,17 @@ function applyKiloCompat<T extends { compat?: ProviderModelConfig["compat"] }>(
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
export default async function kiloProvider(pi: ExtensionAPI) {
|
|
176
|
+
// Resolve API key (env var or ~/.pi/free.json)
|
|
177
|
+
const kiloApiKey = getKiloApiKey();
|
|
178
|
+
|
|
175
179
|
// Try to fetch ALL models at startup (like Cline/OpenRouter)
|
|
176
|
-
//
|
|
180
|
+
// With API key: returns all models; without: returns free-only
|
|
177
181
|
let allModels: ProviderModelConfig[] = [];
|
|
178
182
|
let freeModels: ProviderModelConfig[] = [];
|
|
179
183
|
|
|
180
184
|
try {
|
|
181
185
|
// Fetch all models (returns free-only if no auth, all if auth available)
|
|
182
|
-
allModels = await fetchKiloModels({ freeOnly: false });
|
|
186
|
+
allModels = await fetchKiloModels({ token: kiloApiKey, freeOnly: false });
|
|
183
187
|
// Derive free list using isFreeModel with allModels for detection
|
|
184
188
|
freeModels = allModels.filter((m) =>
|
|
185
189
|
isFreeModel({ ...m, provider: PROVIDER_KILO }, allModels),
|
|
@@ -211,11 +215,12 @@ export default async function kiloProvider(pi: ExtensionAPI) {
|
|
|
211
215
|
baseReRegister(applyKiloCompat(models));
|
|
212
216
|
|
|
213
217
|
// Register with global toggle system
|
|
218
|
+
const hasKiloKey = !!kiloApiKey;
|
|
214
219
|
registerWithGlobalToggle(
|
|
215
220
|
PROVIDER_KILO,
|
|
216
221
|
stored,
|
|
217
222
|
reRegister,
|
|
218
|
-
|
|
223
|
+
hasKiloKey,
|
|
219
224
|
);
|
|
220
225
|
|
|
221
226
|
// OAuth config for Kilo
|
|
@@ -283,14 +288,14 @@ export default async function kiloProvider(pi: ExtensionAPI) {
|
|
|
283
288
|
const modelsWithCompat = applyKiloCompat(currentModels);
|
|
284
289
|
pi.registerProvider(PROVIDER_KILO, {
|
|
285
290
|
baseUrl: KILO_GATEWAY_BASE,
|
|
286
|
-
apiKey: "$KILO_API_KEY",
|
|
291
|
+
apiKey: kiloApiKey || "$KILO_API_KEY",
|
|
287
292
|
api: "openai-completions" as const,
|
|
288
293
|
headers: {
|
|
289
294
|
"X-KILOCODE-EDITORNAME": "Pi",
|
|
290
295
|
"User-Agent": "pi-free-providers",
|
|
291
296
|
},
|
|
292
297
|
models: enhanceWithCI(modelsWithCompat),
|
|
293
|
-
oauth: oauthConfig,
|
|
298
|
+
...(!!kiloApiKey ? {} : { oauth: oauthConfig }),
|
|
294
299
|
});
|
|
295
300
|
|
|
296
301
|
// Registration complete - models registered silently (use LOG_LEVEL=info to see details)
|
|
@@ -138,7 +138,7 @@ export async function fetchOpenRouterModelsWithFree(
|
|
|
138
138
|
|
|
139
139
|
const free = all.filter((m) => {
|
|
140
140
|
const cost = m.cost;
|
|
141
|
-
return cost && cost.input === 0 && cost.output === 0;
|
|
141
|
+
return cost != null && cost.input === 0 && cost.output === 0;
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
return { free, all };
|
|
@@ -226,8 +226,8 @@ function resolvePiAiSubpathFromPackage(specifier: string): string | undefined {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
class DeferredAssistantMessageEventStream {
|
|
229
|
-
private queue: AssistantMessageEvent[] = [];
|
|
230
|
-
private waiting: Array<
|
|
229
|
+
private readonly queue: AssistantMessageEvent[] = [];
|
|
230
|
+
private readonly waiting: Array<
|
|
231
231
|
(result: IteratorResult<AssistantMessageEvent>) => void
|
|
232
232
|
> = [];
|
|
233
233
|
private done = false;
|