pi-free 2.0.10 → 2.0.12
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 +40 -0
- package/README.md +594 -588
- package/banner.svg +1 -1
- package/config.ts +500 -349
- package/constants.ts +108 -106
- package/index.ts +244 -242
- package/lib/built-in-toggle.ts +91 -58
- package/lib/registry.ts +40 -16
- package/lib/util.ts +525 -524
- package/package.json +2 -4
- package/providers/cline/cline-models.ts +3 -10
- package/providers/crofai/crofai.ts +194 -190
- package/providers/deepinfra/deepinfra.ts +208 -206
- package/providers/dynamic-built-in/index.ts +104 -31
- package/providers/model-fetcher.ts +2 -13
- package/providers/novita/novita.ts +205 -0
- package/providers/nvidia/nvidia.ts +4 -6
- package/providers/ollama/ollama.ts +610 -610
- package/providers/ollama/thinking-levels.ts +96 -96
- package/providers/sambanova/sambanova.ts +8 -2
- package/providers/together/together.ts +194 -197
- package/providers/zenmux/zenmux.ts +196 -194
- package/banner.jpg +0 -0
- package/banner.png +0 -0
|
@@ -1,206 +1,208 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DeepInfra Provider Extension
|
|
3
|
-
*
|
|
4
|
-
* DeepInfra is an AI inference cloud with an OpenAI-compatible API for
|
|
5
|
-
* 100+ open-source models (Llama, DeepSeek, Mistral, Qwen, Mixtral, etc.).
|
|
6
|
-
*
|
|
7
|
-
* NOTE: DeepInfra's /v1/openai/models buries real model data in a "metadata"
|
|
8
|
-
* field (context_length, max_tokens, pricing, tags). We extract it here.
|
|
9
|
-
* Pricing is per-MILLION tokens.
|
|
10
|
-
*
|
|
11
|
-
* Free tier:
|
|
12
|
-
* - $5 one-time credit on signup (no credit card)
|
|
13
|
-
* - ~5M tokens, expires after 90 days
|
|
14
|
-
* - 60 RPM (varies by model)
|
|
15
|
-
*
|
|
16
|
-
* Paid: pay-per-token after credits exhaust
|
|
17
|
-
*
|
|
18
|
-
* Endpoint:
|
|
19
|
-
* Chat: https://api.deepinfra.com/v1/openai/chat/completions
|
|
20
|
-
*
|
|
21
|
-
* Setup:
|
|
22
|
-
* 1. Sign up at https://deepinfra.com/ (GitHub or email)
|
|
23
|
-
* 2. Get API key from https://deepinfra.com/dash/api_keys
|
|
24
|
-
* 3. Set DEEPINFRA_TOKEN env var (or add to ~/.pi/free.json)
|
|
25
|
-
*
|
|
26
|
-
* Usage:
|
|
27
|
-
* pi install git:github.com/apmantza/pi-free
|
|
28
|
-
* # Set DEEPINFRA_TOKEN env var
|
|
29
|
-
* # Models appear in /model selector as "deepinfra/meta-llama/..."
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import type {
|
|
33
|
-
ExtensionAPI,
|
|
34
|
-
ProviderModelConfig,
|
|
35
|
-
} from "@earendil-works/pi-coding-agent";
|
|
36
|
-
import { getDeepinfraApiKey } from "../../config.ts";
|
|
37
|
-
import {
|
|
38
|
-
BASE_URL_DEEPINFRA,
|
|
39
|
-
DEFAULT_FETCH_TIMEOUT_MS,
|
|
40
|
-
PROVIDER_DEEPINFRA,
|
|
41
|
-
} from "../../constants.ts";
|
|
42
|
-
import { createLogger } from "../../lib/logger.ts";
|
|
43
|
-
import {
|
|
44
|
-
getProxyModelCompat,
|
|
45
|
-
isLikelyReasoningModel,
|
|
46
|
-
} from "../../lib/provider-compat.ts";
|
|
47
|
-
import { registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
48
|
-
import { fetchWithRetry } from "../../lib/util.ts";
|
|
49
|
-
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
50
|
-
|
|
51
|
-
const _logger = createLogger("deepinfra");
|
|
52
|
-
|
|
53
|
-
// =============================================================================
|
|
54
|
-
// Types
|
|
55
|
-
// =============================================================================
|
|
56
|
-
|
|
57
|
-
interface DeepInfraModel {
|
|
58
|
-
id: string;
|
|
59
|
-
metadata?: {
|
|
60
|
-
context_length?: number;
|
|
61
|
-
max_tokens?: number;
|
|
62
|
-
description?: string;
|
|
63
|
-
pricing?: {
|
|
64
|
-
input_tokens?: number;
|
|
65
|
-
output_tokens?: number;
|
|
66
|
-
};
|
|
67
|
-
tags?: string[];
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// =============================================================================
|
|
72
|
-
// Fetch
|
|
73
|
-
// =============================================================================
|
|
74
|
-
|
|
75
|
-
async function fetchDeepinfraModels(
|
|
76
|
-
apiKey: string,
|
|
77
|
-
): Promise<ProviderModelConfig[]> {
|
|
78
|
-
const response = await fetchWithRetry(
|
|
79
|
-
`${BASE_URL_DEEPINFRA}/models`,
|
|
80
|
-
{
|
|
81
|
-
headers: {
|
|
82
|
-
Authorization: `Bearer ${apiKey}`,
|
|
83
|
-
"Content-Type": "application/json",
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
3,
|
|
87
|
-
1000,
|
|
88
|
-
DEFAULT_FETCH_TIMEOUT_MS,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
if (!response.ok) {
|
|
92
|
-
throw new Error(
|
|
93
|
-
`DeepInfra API error: ${response.status} ${response.statusText}`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const json = (await response.json()) as { data?: DeepInfraModel[] };
|
|
98
|
-
const models = json.data ?? [];
|
|
99
|
-
|
|
100
|
-
_logger.info(`[deepinfra] Fetched ${models.length} models`);
|
|
101
|
-
|
|
102
|
-
return models
|
|
103
|
-
.filter((m) => {
|
|
104
|
-
const id = m.id.toLowerCase();
|
|
105
|
-
// Filter out non-chat models
|
|
106
|
-
if (id.includes("embed")) return false;
|
|
107
|
-
if (id.includes("rerank")) return false;
|
|
108
|
-
if (id.includes("whisper")) return false;
|
|
109
|
-
if (id.includes("speech")) return false;
|
|
110
|
-
return true;
|
|
111
|
-
})
|
|
112
|
-
.map((m): ProviderModelConfig => {
|
|
113
|
-
const meta = m.metadata;
|
|
114
|
-
const name = m.id.split("/").pop() || m.id;
|
|
115
|
-
|
|
116
|
-
// Reasoning: check tags first, fall back to name heuristic
|
|
117
|
-
const reasoning =
|
|
118
|
-
meta?.tags?.includes("reasoning") ??
|
|
119
|
-
isLikelyReasoningModel({ id: m.id, name });
|
|
120
|
-
|
|
121
|
-
// Pricing is per-MILLION tokens. Divide to get per-token (Pi convention).
|
|
122
|
-
const inputCost = (meta?.pricing?.input_tokens ?? 0.3) / 1_000_000;
|
|
123
|
-
const outputCost = (meta?.pricing?.output_tokens ?? 0.9) / 1_000_000;
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
id: m.id,
|
|
127
|
-
name,
|
|
128
|
-
reasoning,
|
|
129
|
-
input: ["text"],
|
|
130
|
-
cost: {
|
|
131
|
-
input: inputCost,
|
|
132
|
-
output: outputCost,
|
|
133
|
-
cacheRead: 0,
|
|
134
|
-
cacheWrite: 0,
|
|
135
|
-
},
|
|
136
|
-
contextWindow: meta?.context_length ?? 128_000,
|
|
137
|
-
maxTokens: meta?.max_tokens ?? 16_384,
|
|
138
|
-
compat: getProxyModelCompat({ id: m.id, name }),
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
const freeModels
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
1
|
+
/**
|
|
2
|
+
* DeepInfra Provider Extension
|
|
3
|
+
*
|
|
4
|
+
* DeepInfra is an AI inference cloud with an OpenAI-compatible API for
|
|
5
|
+
* 100+ open-source models (Llama, DeepSeek, Mistral, Qwen, Mixtral, etc.).
|
|
6
|
+
*
|
|
7
|
+
* NOTE: DeepInfra's /v1/openai/models buries real model data in a "metadata"
|
|
8
|
+
* field (context_length, max_tokens, pricing, tags). We extract it here.
|
|
9
|
+
* Pricing is per-MILLION tokens.
|
|
10
|
+
*
|
|
11
|
+
* Free tier:
|
|
12
|
+
* - $5 one-time credit on signup (no credit card)
|
|
13
|
+
* - ~5M tokens, expires after 90 days
|
|
14
|
+
* - 60 RPM (varies by model)
|
|
15
|
+
*
|
|
16
|
+
* Paid: pay-per-token after credits exhaust
|
|
17
|
+
*
|
|
18
|
+
* Endpoint:
|
|
19
|
+
* Chat: https://api.deepinfra.com/v1/openai/chat/completions
|
|
20
|
+
*
|
|
21
|
+
* Setup:
|
|
22
|
+
* 1. Sign up at https://deepinfra.com/ (GitHub or email)
|
|
23
|
+
* 2. Get API key from https://deepinfra.com/dash/api_keys
|
|
24
|
+
* 3. Set DEEPINFRA_TOKEN env var (or add to ~/.pi/free.json)
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* pi install git:github.com/apmantza/pi-free
|
|
28
|
+
* # Set DEEPINFRA_TOKEN env var
|
|
29
|
+
* # Models appear in /model selector as "deepinfra/meta-llama/..."
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type {
|
|
33
|
+
ExtensionAPI,
|
|
34
|
+
ProviderModelConfig,
|
|
35
|
+
} from "@earendil-works/pi-coding-agent";
|
|
36
|
+
import { getDeepinfraApiKey } from "../../config.ts";
|
|
37
|
+
import {
|
|
38
|
+
BASE_URL_DEEPINFRA,
|
|
39
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
40
|
+
PROVIDER_DEEPINFRA,
|
|
41
|
+
} from "../../constants.ts";
|
|
42
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
43
|
+
import {
|
|
44
|
+
getProxyModelCompat,
|
|
45
|
+
isLikelyReasoningModel,
|
|
46
|
+
} from "../../lib/provider-compat.ts";
|
|
47
|
+
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
48
|
+
import { fetchWithRetry } from "../../lib/util.ts";
|
|
49
|
+
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
50
|
+
|
|
51
|
+
const _logger = createLogger("deepinfra");
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// Types
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
interface DeepInfraModel {
|
|
58
|
+
id: string;
|
|
59
|
+
metadata?: {
|
|
60
|
+
context_length?: number;
|
|
61
|
+
max_tokens?: number;
|
|
62
|
+
description?: string;
|
|
63
|
+
pricing?: {
|
|
64
|
+
input_tokens?: number;
|
|
65
|
+
output_tokens?: number;
|
|
66
|
+
};
|
|
67
|
+
tags?: string[];
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Fetch
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
async function fetchDeepinfraModels(
|
|
76
|
+
apiKey: string,
|
|
77
|
+
): Promise<ProviderModelConfig[]> {
|
|
78
|
+
const response = await fetchWithRetry(
|
|
79
|
+
`${BASE_URL_DEEPINFRA}/models`,
|
|
80
|
+
{
|
|
81
|
+
headers: {
|
|
82
|
+
Authorization: `Bearer ${apiKey}`,
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
3,
|
|
87
|
+
1000,
|
|
88
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`DeepInfra API error: ${response.status} ${response.statusText}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const json = (await response.json()) as { data?: DeepInfraModel[] };
|
|
98
|
+
const models = json.data ?? [];
|
|
99
|
+
|
|
100
|
+
_logger.info(`[deepinfra] Fetched ${models.length} models`);
|
|
101
|
+
|
|
102
|
+
return models
|
|
103
|
+
.filter((m) => {
|
|
104
|
+
const id = m.id.toLowerCase();
|
|
105
|
+
// Filter out non-chat models
|
|
106
|
+
if (id.includes("embed")) return false;
|
|
107
|
+
if (id.includes("rerank")) return false;
|
|
108
|
+
if (id.includes("whisper")) return false;
|
|
109
|
+
if (id.includes("speech")) return false;
|
|
110
|
+
return true;
|
|
111
|
+
})
|
|
112
|
+
.map((m): ProviderModelConfig => {
|
|
113
|
+
const meta = m.metadata;
|
|
114
|
+
const name = m.id.split("/").pop() || m.id;
|
|
115
|
+
|
|
116
|
+
// Reasoning: check tags first, fall back to name heuristic
|
|
117
|
+
const reasoning =
|
|
118
|
+
meta?.tags?.includes("reasoning") ??
|
|
119
|
+
isLikelyReasoningModel({ id: m.id, name });
|
|
120
|
+
|
|
121
|
+
// Pricing is per-MILLION tokens. Divide to get per-token (Pi convention).
|
|
122
|
+
const inputCost = (meta?.pricing?.input_tokens ?? 0.3) / 1_000_000;
|
|
123
|
+
const outputCost = (meta?.pricing?.output_tokens ?? 0.9) / 1_000_000;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
id: m.id,
|
|
127
|
+
name,
|
|
128
|
+
reasoning,
|
|
129
|
+
input: ["text"],
|
|
130
|
+
cost: {
|
|
131
|
+
input: inputCost,
|
|
132
|
+
output: outputCost,
|
|
133
|
+
cacheRead: 0,
|
|
134
|
+
cacheWrite: 0,
|
|
135
|
+
},
|
|
136
|
+
contextWindow: meta?.context_length ?? 128_000,
|
|
137
|
+
maxTokens: meta?.max_tokens ?? 16_384,
|
|
138
|
+
compat: getProxyModelCompat({ id: m.id, name }),
|
|
139
|
+
_pricingKnown: meta?.pricing !== undefined,
|
|
140
|
+
} as ProviderModelConfig & { _pricingKnown?: boolean };
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// Extension Entry Point
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
export default async function deepinfraProvider(pi: ExtensionAPI) {
|
|
149
|
+
const apiKey = getDeepinfraApiKey();
|
|
150
|
+
|
|
151
|
+
if (!apiKey) {
|
|
152
|
+
_logger.info(
|
|
153
|
+
"[deepinfra] Skipping — DEEPINFRA_TOKEN not set. Sign up at https://deepinfra.com/",
|
|
154
|
+
);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Fetch models
|
|
159
|
+
const allModels = await fetchDeepinfraModels(apiKey);
|
|
160
|
+
|
|
161
|
+
if (allModels.length === 0) {
|
|
162
|
+
_logger.warn("[deepinfra] No chat models available");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// DeepInfra is a trial credit provider — $5 one-time credit, no truly free models.
|
|
167
|
+
// Use isFreeModel for consistent detection across all providers.
|
|
168
|
+
const freeModels = allModels.filter((m) =>
|
|
169
|
+
isFreeModel({ ...m, provider: PROVIDER_DEEPINFRA }, allModels),
|
|
170
|
+
);
|
|
171
|
+
const stored = { free: freeModels, all: allModels };
|
|
172
|
+
|
|
173
|
+
_logger.info(
|
|
174
|
+
`[deepinfra] Registered ${allModels.length} chat models (trial credit, 0 free)`,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Create re-register function
|
|
178
|
+
const reRegister = createReRegister(pi, {
|
|
179
|
+
providerId: PROVIDER_DEEPINFRA,
|
|
180
|
+
baseUrl: BASE_URL_DEEPINFRA,
|
|
181
|
+
apiKey,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Register with global toggle
|
|
185
|
+
registerWithGlobalToggle(PROVIDER_DEEPINFRA, stored, reRegister, true);
|
|
186
|
+
|
|
187
|
+
// Setup provider with toggle command
|
|
188
|
+
setupProvider(
|
|
189
|
+
pi,
|
|
190
|
+
{
|
|
191
|
+
providerId: PROVIDER_DEEPINFRA,
|
|
192
|
+
initialShowPaid: true, // trial credit: default to showing all models
|
|
193
|
+
tosUrl: "https://deepinfra.com/pricing",
|
|
194
|
+
reRegister: (models, _stored) => {
|
|
195
|
+
if (_stored) {
|
|
196
|
+
stored.free = _stored.free;
|
|
197
|
+
stored.all = _stored.all;
|
|
198
|
+
}
|
|
199
|
+
reRegister(models);
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
stored,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Initial registration — DeepInfra is a trial-credit provider,
|
|
206
|
+
// so always show all models. Users see them immediately on setup.
|
|
207
|
+
reRegister(allModels);
|
|
208
|
+
}
|
|
@@ -5,15 +5,18 @@
|
|
|
5
5
|
* standard /models endpoints when the user has configured an API key.
|
|
6
6
|
*
|
|
7
7
|
* Uses a single generic fetch function instead of per-provider boilerplate.
|
|
8
|
-
* Discovery runs concurrently
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Discovery runs concurrently and is awaited by the extension entry point.
|
|
9
|
+
* Pi only flushes provider registrations after async extension startup, so
|
|
10
|
+
* dynamic providers must register before setup returns.
|
|
11
11
|
*
|
|
12
12
|
* Providers handled:
|
|
13
13
|
* - mistral (MISTRAL_API_KEY)
|
|
14
14
|
* - groq (GROQ_API_KEY)
|
|
15
15
|
* - cerebras (CEREBRAS_API_KEY)
|
|
16
16
|
* - xai (XAI_API_KEY)
|
|
17
|
+
* - opencode (OPENCODE_API_KEY from auth.json)
|
|
18
|
+
* - openrouter (OPENROUTER_API_KEY from auth.json)
|
|
19
|
+
* - fastrouter (always discovered, FASTROUTER_API_KEY)
|
|
17
20
|
* - huggingface (HF_TOKEN - optional, special-cased API shape)
|
|
18
21
|
*
|
|
19
22
|
* OpenAI is intentionally skipped per user request.
|
|
@@ -25,14 +28,22 @@ import type {
|
|
|
25
28
|
} from "@earendil-works/pi-coding-agent";
|
|
26
29
|
import {
|
|
27
30
|
getCerebrasApiKey,
|
|
31
|
+
getFastrouterApiKey,
|
|
32
|
+
getFastrouterShowPaid,
|
|
28
33
|
getGroqApiKey,
|
|
29
34
|
getHfToken,
|
|
30
35
|
getMistralApiKey,
|
|
36
|
+
getOpencodeApiKey,
|
|
37
|
+
getOpencodeShowPaid,
|
|
38
|
+
getOpenrouterApiKey,
|
|
39
|
+
getOpenrouterShowPaid,
|
|
31
40
|
getXaiApiKey,
|
|
32
41
|
} from "../../config.ts";
|
|
42
|
+
import { DEFAULT_FETCH_TIMEOUT_MS } from "../../constants.ts";
|
|
33
43
|
import { createLogger } from "../../lib/logger.ts";
|
|
34
44
|
import { getProxyModelCompat } from "../../lib/provider-compat.ts";
|
|
35
45
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
46
|
+
import { fetchOpenRouterCompatibleModels } from "../model-fetcher.ts";
|
|
36
47
|
import { createToggleState } from "../../lib/toggle-state.ts";
|
|
37
48
|
import { enhanceWithCI } from "../../provider-helper.ts";
|
|
38
49
|
|
|
@@ -68,7 +79,7 @@ async function fetchModelsFromEndpoint(
|
|
|
68
79
|
|
|
69
80
|
const response = await fetch(url, {
|
|
70
81
|
headers,
|
|
71
|
-
signal: AbortSignal.timeout(opts.timeoutMs ??
|
|
82
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS),
|
|
72
83
|
});
|
|
73
84
|
|
|
74
85
|
if (!response.ok) {
|
|
@@ -101,9 +112,10 @@ async function fetchModelsFromEndpoint(
|
|
|
101
112
|
((m.max_tokens ?? m.max_completion_tokens) as number) ??
|
|
102
113
|
opts.modelDefaults?.maxTokens ??
|
|
103
114
|
16_384,
|
|
115
|
+
_pricingKnown: false as boolean | undefined,
|
|
104
116
|
...opts.modelDefaults,
|
|
105
117
|
...(opts.compat ? { compat: opts.compat } : {}),
|
|
106
|
-
} satisfies ProviderModelConfig;
|
|
118
|
+
} satisfies ProviderModelConfig & { _pricingKnown?: boolean };
|
|
107
119
|
});
|
|
108
120
|
}
|
|
109
121
|
|
|
@@ -123,7 +135,7 @@ async function fetchHuggingFaceModels(
|
|
|
123
135
|
|
|
124
136
|
const response = await fetch(
|
|
125
137
|
"https://api-inference.huggingface.co/models?pipeline_tag=text-generation&limit=50",
|
|
126
|
-
{ headers, signal: AbortSignal.timeout(
|
|
138
|
+
{ headers, signal: AbortSignal.timeout(DEFAULT_FETCH_TIMEOUT_MS) },
|
|
127
139
|
);
|
|
128
140
|
|
|
129
141
|
if (!response.ok) {
|
|
@@ -159,11 +171,16 @@ interface DynamicProviderDef {
|
|
|
159
171
|
getApiKey: () => string | undefined;
|
|
160
172
|
baseUrl: string;
|
|
161
173
|
api: "openai-completions" | "mistral-conversations" | "anthropic-messages";
|
|
162
|
-
defaultShowPaid: boolean;
|
|
174
|
+
defaultShowPaid: boolean | (() => boolean);
|
|
163
175
|
/** Optional per-provider compat overrides (e.g., DeepSeek proxy). */
|
|
164
176
|
compat?: ProviderModelConfig["compat"];
|
|
165
177
|
/** Per-model field defaults when the API doesn't expose them. */
|
|
166
178
|
modelDefaults?: Partial<ProviderModelConfig>;
|
|
179
|
+
/**
|
|
180
|
+
* Custom model fetcher (e.g., OpenRouter uses its own pricing-aware fetcher).
|
|
181
|
+
* When not provided, fetchModelsFromEndpoint is used (no pricing, _pricingKnown=false).
|
|
182
|
+
*/
|
|
183
|
+
fetchModels?: (apiKey: string) => Promise<ProviderModelConfig[]>;
|
|
167
184
|
}
|
|
168
185
|
|
|
169
186
|
const DYNAMIC_PROVIDERS: DynamicProviderDef[] = [
|
|
@@ -196,6 +213,28 @@ const DYNAMIC_PROVIDERS: DynamicProviderDef[] = [
|
|
|
196
213
|
api: "openai-completions",
|
|
197
214
|
defaultShowPaid: false,
|
|
198
215
|
},
|
|
216
|
+
{
|
|
217
|
+
providerId: "opencode",
|
|
218
|
+
getApiKey: getOpencodeApiKey,
|
|
219
|
+
baseUrl: "https://opencode.ai/zen/v1",
|
|
220
|
+
api: "openai-completions",
|
|
221
|
+
defaultShowPaid: getOpencodeShowPaid,
|
|
222
|
+
// OpenCode API returns no pricing — _pricingKnown=false, name-based detection
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
providerId: "openrouter",
|
|
226
|
+
getApiKey: getOpenrouterApiKey,
|
|
227
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
228
|
+
api: "openai-completions",
|
|
229
|
+
defaultShowPaid: getOpenrouterShowPaid,
|
|
230
|
+
// OpenRouter returns full pricing — use its dedicated fetcher
|
|
231
|
+
fetchModels: (apiKey) =>
|
|
232
|
+
fetchOpenRouterCompatibleModels({
|
|
233
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
234
|
+
apiKey,
|
|
235
|
+
freeOnly: false,
|
|
236
|
+
}),
|
|
237
|
+
},
|
|
199
238
|
];
|
|
200
239
|
|
|
201
240
|
// =============================================================================
|
|
@@ -210,22 +249,27 @@ async function discoverAndRegister(
|
|
|
210
249
|
let allModels: ProviderModelConfig[];
|
|
211
250
|
|
|
212
251
|
try {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
252
|
+
if (config.fetchModels) {
|
|
253
|
+
allModels = await config.fetchModels(apiKey);
|
|
254
|
+
} else {
|
|
255
|
+
allModels = await fetchModelsFromEndpoint({
|
|
256
|
+
baseUrl: config.baseUrl,
|
|
257
|
+
apiKey,
|
|
258
|
+
compat: config.compat,
|
|
259
|
+
modelDefaults: config.modelDefaults,
|
|
260
|
+
timeoutMs: DEFAULT_FETCH_TIMEOUT_MS,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
220
263
|
|
|
221
264
|
// Apply DeepSeek proxy compat to matching models
|
|
222
265
|
allModels = allModels.map((m) => ({
|
|
223
266
|
...m,
|
|
224
267
|
compat: getProxyModelCompat(m) ?? m.compat,
|
|
225
268
|
}));
|
|
226
|
-
} catch {
|
|
269
|
+
} catch (error) {
|
|
227
270
|
_logger.info(
|
|
228
271
|
`[dynamic] ${config.providerId}: discovery failed, Pi keeps its defaults`,
|
|
272
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
229
273
|
);
|
|
230
274
|
return;
|
|
231
275
|
}
|
|
@@ -248,9 +292,10 @@ async function discoverAndRegisterHF(
|
|
|
248
292
|
let allModels: ProviderModelConfig[];
|
|
249
293
|
try {
|
|
250
294
|
allModels = await fetchHuggingFaceModels(apiKey);
|
|
251
|
-
} catch {
|
|
295
|
+
} catch (error) {
|
|
252
296
|
_logger.info(
|
|
253
297
|
"[dynamic] huggingface: discovery failed, Pi keeps its defaults",
|
|
298
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
254
299
|
);
|
|
255
300
|
return;
|
|
256
301
|
}
|
|
@@ -289,7 +334,10 @@ async function registerProvider(
|
|
|
289
334
|
// Toggle state
|
|
290
335
|
const toggleState = createToggleState({
|
|
291
336
|
providerId: config.providerId,
|
|
292
|
-
initialShowPaid:
|
|
337
|
+
initialShowPaid:
|
|
338
|
+
typeof config.defaultShowPaid === "function"
|
|
339
|
+
? config.defaultShowPaid()
|
|
340
|
+
: config.defaultShowPaid,
|
|
293
341
|
initialModels: { free: freeModels, all: allModels },
|
|
294
342
|
});
|
|
295
343
|
|
|
@@ -341,16 +389,18 @@ async function registerProvider(
|
|
|
341
389
|
}
|
|
342
390
|
|
|
343
391
|
// =============================================================================
|
|
344
|
-
// Main Entry
|
|
392
|
+
// Main Entry
|
|
345
393
|
// =============================================================================
|
|
346
394
|
|
|
347
395
|
/**
|
|
348
396
|
* Kick off model discovery for all configured providers.
|
|
349
|
-
* Runs each fetch concurrently
|
|
350
|
-
*
|
|
397
|
+
* Runs each fetch concurrently so startup waits for the slowest provider,
|
|
398
|
+
* not `n * provider latency`.
|
|
351
399
|
*
|
|
352
|
-
* Pi
|
|
353
|
-
* function
|
|
400
|
+
* Pi flushes provider registrations after async extension startup completes,
|
|
401
|
+
* so this function must await discovery before returning. Otherwise late
|
|
402
|
+
* pi.registerProvider() calls may not be visible to startup flows such as
|
|
403
|
+
* `pi --list-models` or the initial model picker.
|
|
354
404
|
*/
|
|
355
405
|
export async function setupDynamicBuiltInProviders(
|
|
356
406
|
pi: ExtensionAPI,
|
|
@@ -368,18 +418,41 @@ export async function setupDynamicBuiltInProviders(
|
|
|
368
418
|
fetchers.push(discoverAndRegisterHF(pi, hfKey));
|
|
369
419
|
}
|
|
370
420
|
|
|
421
|
+
// FastRouter: always discovered (model listing needs no auth), but Pi
|
|
422
|
+
// requires a non-empty apiKey/env-var name when replacing a provider's models.
|
|
423
|
+
// Use the real configured key when present; otherwise register with the env
|
|
424
|
+
// var name so startup does not fail for users who have not configured it yet.
|
|
425
|
+
const fastrouterApiKey = getFastrouterApiKey();
|
|
426
|
+
fetchers.push(
|
|
427
|
+
discoverAndRegister(
|
|
428
|
+
pi,
|
|
429
|
+
{
|
|
430
|
+
providerId: "fastrouter",
|
|
431
|
+
getApiKey: getFastrouterApiKey,
|
|
432
|
+
baseUrl: "https://api.fastrouter.ai/api/v1",
|
|
433
|
+
api: "openai-completions",
|
|
434
|
+
defaultShowPaid: getFastrouterShowPaid,
|
|
435
|
+
fetchModels: () =>
|
|
436
|
+
fetchOpenRouterCompatibleModels({
|
|
437
|
+
baseUrl: "https://api.fastrouter.ai/api/v1",
|
|
438
|
+
apiKey: fastrouterApiKey,
|
|
439
|
+
freeOnly: false,
|
|
440
|
+
}),
|
|
441
|
+
},
|
|
442
|
+
fastrouterApiKey ?? "FASTROUTER_API_KEY",
|
|
443
|
+
),
|
|
444
|
+
);
|
|
445
|
+
|
|
371
446
|
if (fetchers.length === 0) return;
|
|
372
447
|
|
|
373
448
|
_logger.info(
|
|
374
|
-
`[dynamic] Kicking off discovery for ${fetchers.length} providers (
|
|
449
|
+
`[dynamic] Kicking off discovery for ${fetchers.length} providers (concurrent)...`,
|
|
375
450
|
);
|
|
376
451
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
);
|
|
384
|
-
});
|
|
452
|
+
const results = await Promise.allSettled(fetchers);
|
|
453
|
+
const succeeded = results.filter((r) => r.status === "fulfilled").length;
|
|
454
|
+
const failed = results.filter((r) => r.status === "rejected").length;
|
|
455
|
+
_logger.info(
|
|
456
|
+
`[dynamic] Discovery complete: ${succeeded} succeeded, ${failed} failed/rejected`,
|
|
457
|
+
);
|
|
385
458
|
}
|