pi-free 2.2.6 → 2.2.7
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 +19 -4
- package/README.md +3 -2
- package/config.ts +22 -4
- package/constants.ts +3 -0
- package/index.ts +6 -7
- package/package.json +74 -74
- package/providers/anyapi/anyapi.ts +270 -0
- package/providers/kilo/kilo.ts +1 -6
- package/providers/opencode-session.ts +69 -18
- package/providers/tokenrouter/tokenrouter.ts +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,17 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.2.7] - 2026-07-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **AnyAPI provider** — add AnyAPI's OpenAI-compatible gateway with dynamic model discovery, free-model filtering, a 100K-token daily free plan, and `/toggle-anyapi` for switching to the full catalog.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Pi 0.80 compatibility** — update the Pi SDK peer requirements to `>=0.80.0` and refresh the development toolchain dependencies.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **AnyAPI model limits** — enrich context and output limits from models.dev when AnyAPI omits them, avoiding incorrect 4K defaults and migrating existing caches once.
|
|
23
|
+
- **TokenRouter Pi AI compatibility** — use the supported lazy OpenAI completions API path for Pi 0.80.
|
|
24
|
+
|
|
10
25
|
## [2.2.6] - 2026-07-09
|
|
11
26
|
|
|
12
27
|
### Changed
|
|
13
28
|
|
|
14
|
-
- **Faster startup (~30x)** — warm-cache load dropped from ~2.1s to ~70ms: providers now serve from a 1-hour disk cache and fetch live only on cold/stale cache. Extends the cache-first pattern (already used by Cline) to kilo, fastrouter, and all OpenAI-compatible fetchers (tokenrouter, zenmux, crofai, deepinfra, sambanova, together, novita, routeway, bai, openmodel); the dynamic built-in phase runs concurrently with the static providers.
|
|
15
|
-
- **Coding-Index debug logging is now opt-in** — `~/.pi/modelmatch.log` previously received one synchronous `appendFileSync` per model per match attempt at startup. Now off by default; set `PI_FREE_BENCHMARK_DEBUG=1` to re-enable.
|
|
16
|
-
- **Config reads are memoized** — `~/.pi/free.json` is parsed once and reused while its mtime is unchanged.
|
|
29
|
+
- **Faster startup (~30x)** — warm-cache load dropped from ~2.1s to ~70ms: providers now serve from a 1-hour disk cache and fetch live only on cold/stale cache. Extends the cache-first pattern (already used by Cline) to kilo, fastrouter, and all OpenAI-compatible fetchers (tokenrouter, zenmux, crofai, deepinfra, sambanova, together, novita, routeway, bai, openmodel); the dynamic built-in phase runs concurrently with the static providers. Thanks @lmilojevicc
|
|
30
|
+
- **Coding-Index debug logging is now opt-in** — `~/.pi/modelmatch.log` previously received one synchronous `appendFileSync` per model per match attempt at startup. Now off by default; set `PI_FREE_BENCHMARK_DEBUG=1` to re-enable. Thanks @lmilojevicc
|
|
31
|
+
- **Config reads are memoized** — `~/.pi/free.json` is parsed once and reused while its mtime is unchanged. Thanks @lmilojevicc
|
|
17
32
|
|
|
18
33
|
### Fixed
|
|
19
34
|
|
|
20
|
-
- **Cache-poisoning guard** — a transient partial API response (a 200 returning a near-empty list) can no longer overwrite a healthy cached model list; fetches returning < 50% of the cached count keep the existing cache.
|
|
35
|
+
- **Cache-poisoning guard** — a transient partial API response (a 200 returning a near-empty list) can no longer overwrite a healthy cached model list; fetches returning < 50% of the cached count keep the existing cache. Thanks @lmilojevicc
|
|
21
36
|
|
|
22
37
|
### Removed
|
|
23
38
|
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Free and paid AI model providers for [Pi](https://pi.dev). Access **free and pai
|
|
|
14
14
|
|
|
15
15
|
When you install pi-free, it:
|
|
16
16
|
|
|
17
|
-
1. **Registers free-tier providers** — Kilo, Cline, LLM7, OpenModel, TokenRouter, and more
|
|
17
|
+
1. **Registers free-tier providers** — Kilo, Cline, LLM7, OpenModel, TokenRouter, AnyAPI, and more
|
|
18
18
|
2. **Captures Pi's built-in providers** with free/paid toggles — OpenCode, OpenRouter
|
|
19
19
|
3. **Fetches models dynamically** — ZenMux, CrofAI, Mistral, Groq, Cerebras, xAI, Hugging Face when API keys are configured
|
|
20
20
|
4. **Filters to show only free models by default** — paid models hidden until explicitly toggled on
|
|
@@ -62,6 +62,7 @@ First run creates `~/.pi/free.json` automatically. Add keys there or use environ
|
|
|
62
62
|
```json
|
|
63
63
|
{
|
|
64
64
|
"ollama_api_key": "...",
|
|
65
|
+
"anyapi_api_key": "...",
|
|
65
66
|
"sambanova_api_key": "...",
|
|
66
67
|
"deepinfra_api_key": "..."
|
|
67
68
|
}
|
|
@@ -74,7 +75,7 @@ First run creates `~/.pi/free.json` automatically. Add keys there or use environ
|
|
|
74
75
|
| Category | Providers |
|
|
75
76
|
|---|---|
|
|
76
77
|
| ✅ **Free** | Kilo, Cline, OpenRouter, OpenCode, LLM7, OpenModel, TokenRouter (1 free) |
|
|
77
|
-
| 🔄 **Freemium** | Ollama Cloud, SambaNova
|
|
78
|
+
| 🔄 **Freemium** | AnyAPI, Ollama Cloud, SambaNova |
|
|
78
79
|
| 💳 **Paid** | ZenMux, CrofAI, DeepInfra, Together, Novita, Routeway, b.ai |
|
|
79
80
|
| 🔧 **Dynamic** | Mistral, Groq, Cerebras, xAI, Hugging Face, FastRouter |
|
|
80
81
|
|
package/config.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import {
|
|
21
|
+
PROVIDER_ANYAPI,
|
|
21
22
|
PROVIDER_BAI,
|
|
22
23
|
PROVIDER_CLINE,
|
|
23
24
|
PROVIDER_FASTROUTER,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
PROVIDER_NOVITA,
|
|
39
40
|
} from "./constants.ts";
|
|
40
41
|
export {
|
|
42
|
+
PROVIDER_ANYAPI,
|
|
41
43
|
PROVIDER_BAI,
|
|
42
44
|
PROVIDER_CLINE,
|
|
43
45
|
PROVIDER_FASTROUTER,
|
|
@@ -78,6 +80,7 @@ interface PiFreeConfig {
|
|
|
78
80
|
routeway_api_key?: string;
|
|
79
81
|
fastrouter_api_key?: string;
|
|
80
82
|
tokenrouter_api_key?: string;
|
|
83
|
+
anyapi_api_key?: string;
|
|
81
84
|
bai_api_key?: string;
|
|
82
85
|
openmodel_api_key?: string;
|
|
83
86
|
kilo_api_key?: string;
|
|
@@ -98,6 +101,7 @@ interface PiFreeConfig {
|
|
|
98
101
|
routeway_show_paid?: boolean;
|
|
99
102
|
fastrouter_show_paid?: boolean;
|
|
100
103
|
tokenrouter_show_paid?: boolean;
|
|
104
|
+
anyapi_show_paid?: boolean;
|
|
101
105
|
bai_show_paid?: boolean;
|
|
102
106
|
openmodel_show_paid?: boolean;
|
|
103
107
|
openrouter_show_paid?: boolean;
|
|
@@ -118,6 +122,7 @@ const CONFIG_TEMPLATE: PiFreeConfig = {
|
|
|
118
122
|
routeway_api_key: "",
|
|
119
123
|
fastrouter_api_key: "",
|
|
120
124
|
tokenrouter_api_key: "",
|
|
125
|
+
anyapi_api_key: "",
|
|
121
126
|
bai_api_key: "",
|
|
122
127
|
openmodel_api_key: "",
|
|
123
128
|
kilo_api_key: "",
|
|
@@ -139,6 +144,7 @@ const CONFIG_TEMPLATE: PiFreeConfig = {
|
|
|
139
144
|
routeway_show_paid: false,
|
|
140
145
|
fastrouter_show_paid: false,
|
|
141
146
|
tokenrouter_show_paid: false,
|
|
147
|
+
anyapi_show_paid: false,
|
|
142
148
|
bai_show_paid: false,
|
|
143
149
|
openmodel_show_paid: false,
|
|
144
150
|
openrouter_show_paid: false,
|
|
@@ -249,10 +255,13 @@ export function loadConfigFile(): PiFreeConfig {
|
|
|
249
255
|
cachedConfig = null;
|
|
250
256
|
cachedConfigMtime = -1;
|
|
251
257
|
if (err instanceof SyntaxError) {
|
|
252
|
-
_logger.error(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
258
|
+
_logger.error(
|
|
259
|
+
"Config file is corrupt (invalid JSON) — returning empty config",
|
|
260
|
+
{
|
|
261
|
+
path: CONFIG_PATH,
|
|
262
|
+
error: err.message,
|
|
263
|
+
},
|
|
264
|
+
);
|
|
256
265
|
} else {
|
|
257
266
|
_logger.error("Could not read config file — returning empty config", {
|
|
258
267
|
path: CONFIG_PATH,
|
|
@@ -342,6 +351,7 @@ const PROVIDER_META: readonly ProviderMeta[] = [
|
|
|
342
351
|
prefix: "TOKENROUTER",
|
|
343
352
|
showPaidKey: "tokenrouter_show_paid",
|
|
344
353
|
},
|
|
354
|
+
{ id: PROVIDER_ANYAPI, prefix: "ANYAPI", showPaidKey: "anyapi_show_paid" },
|
|
345
355
|
{ id: PROVIDER_BAI, prefix: "BAI", showPaidKey: "bai_show_paid" },
|
|
346
356
|
{
|
|
347
357
|
id: PROVIDER_OPENMODEL,
|
|
@@ -441,6 +451,10 @@ export function getTokenrouterShowPaid(): boolean {
|
|
|
441
451
|
);
|
|
442
452
|
}
|
|
443
453
|
|
|
454
|
+
export function getAnyapiShowPaid(): boolean {
|
|
455
|
+
return resolveBool("ANYAPI_SHOW_PAID", loadConfigFile().anyapi_show_paid);
|
|
456
|
+
}
|
|
457
|
+
|
|
444
458
|
export function getBaiShowPaid(): boolean {
|
|
445
459
|
return resolveBool("BAI_SHOW_PAID", loadConfigFile().bai_show_paid);
|
|
446
460
|
}
|
|
@@ -538,6 +552,10 @@ export function getTokenrouterApiKey(): string | undefined {
|
|
|
538
552
|
return resolve("TOKENROUTER_API_KEY", loadConfigFile().tokenrouter_api_key);
|
|
539
553
|
}
|
|
540
554
|
|
|
555
|
+
export function getAnyapiApiKey(): string | undefined {
|
|
556
|
+
return resolve("ANYAPI_API_KEY", loadConfigFile().anyapi_api_key);
|
|
557
|
+
}
|
|
558
|
+
|
|
541
559
|
export function getBaiApiKey(): string | undefined {
|
|
542
560
|
return resolve("BAI_API_KEY", loadConfigFile().bai_api_key);
|
|
543
561
|
}
|
package/constants.ts
CHANGED
|
@@ -23,6 +23,7 @@ export const PROVIDER_TOGETHER = "together";
|
|
|
23
23
|
export const PROVIDER_NOVITA = "novita";
|
|
24
24
|
export const PROVIDER_ROUTEWAY = "routeway";
|
|
25
25
|
export const PROVIDER_TOKENROUTER = "tokenrouter";
|
|
26
|
+
export const PROVIDER_ANYAPI = "anyapi";
|
|
26
27
|
export const PROVIDER_BAI = "bai";
|
|
27
28
|
export const PROVIDER_OPENMODEL = "openmodel";
|
|
28
29
|
export const PROVIDER_QODER = "qoder";
|
|
@@ -48,6 +49,7 @@ export const ALL_UNIQUE_PROVIDERS = [
|
|
|
48
49
|
PROVIDER_NOVITA,
|
|
49
50
|
PROVIDER_ROUTEWAY,
|
|
50
51
|
PROVIDER_TOKENROUTER,
|
|
52
|
+
PROVIDER_ANYAPI,
|
|
51
53
|
PROVIDER_BAI,
|
|
52
54
|
PROVIDER_OPENMODEL,
|
|
53
55
|
PROVIDER_QODER,
|
|
@@ -73,6 +75,7 @@ export const BASE_URL_TOGETHER = "https://api.together.xyz/v1";
|
|
|
73
75
|
export const BASE_URL_NOVITA = "https://api.novita.ai/openai/v1";
|
|
74
76
|
export const BASE_URL_ROUTEWAY = "https://api.routeway.ai/v1";
|
|
75
77
|
export const BASE_URL_TOKENROUTER = "https://api.tokenrouter.com/v1";
|
|
78
|
+
export const BASE_URL_ANYAPI = "https://api.anyapi.ai/v1";
|
|
76
79
|
export const BASE_URL_BAI = "https://api.b.ai/v1";
|
|
77
80
|
/**
|
|
78
81
|
* OpenModel is registered with `api: "anthropic-messages"`. The pi-ai
|
package/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ import tokenRouter from "./providers/tokenrouter/tokenrouter.ts";
|
|
|
43
43
|
import ollama from "./providers/ollama/ollama.ts";
|
|
44
44
|
import zenmux from "./providers/zenmux/zenmux.ts";
|
|
45
45
|
import bai from "./providers/bai/bai.ts";
|
|
46
|
+
import anyapi from "./providers/anyapi/anyapi.ts";
|
|
46
47
|
import openmodel from "./providers/openmodel/openmodel.ts";
|
|
47
48
|
import qoder from "./providers/qoder/qoder.ts";
|
|
48
49
|
|
|
@@ -67,6 +68,7 @@ const UNIQUE_PROVIDERS: ReadonlyArray<(pi: ExtensionAPI) => Promise<void>> = [
|
|
|
67
68
|
novita,
|
|
68
69
|
routeway,
|
|
69
70
|
tokenRouter,
|
|
71
|
+
anyapi,
|
|
70
72
|
bai,
|
|
71
73
|
openmodel,
|
|
72
74
|
qoder,
|
|
@@ -400,13 +402,10 @@ export default async function piFreeEntry(pi: ExtensionAPI) {
|
|
|
400
402
|
// already-registered static providers rather than failing the whole
|
|
401
403
|
// extension load. Log full error (message + stack) to the structured
|
|
402
404
|
// log so the user can investigate, but never block startup.
|
|
403
|
-
_logger.error(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
stack: err instanceof Error ? err.stack : undefined,
|
|
408
|
-
},
|
|
409
|
-
);
|
|
405
|
+
_logger.error("[pi-free] Dynamic built-in providers failed to load", {
|
|
406
|
+
error: err instanceof Error ? err.message : String(err),
|
|
407
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
408
|
+
});
|
|
410
409
|
}
|
|
411
410
|
})();
|
|
412
411
|
|
package/package.json
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-free",
|
|
3
|
-
"version": "2.2.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "AI model providers for Pi with free model filtering and dynamic model fetching",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"pi-package",
|
|
8
|
-
"pi-extension",
|
|
9
|
-
"free-models",
|
|
10
|
-
"paid-models",
|
|
11
|
-
"model-filter",
|
|
12
|
-
"nvidia-nim",
|
|
13
|
-
"kilo",
|
|
14
|
-
"cline",
|
|
15
|
-
"zenmux",
|
|
16
|
-
"crofai",
|
|
17
|
-
"ollama-cloud"
|
|
18
|
-
],
|
|
19
|
-
"license": "MIT",
|
|
20
|
-
"author": "Apostolos Mantzaris",
|
|
21
|
-
"homepage": "https://github.com/apmantza/pi-free#readme",
|
|
22
|
-
"bugs": {
|
|
23
|
-
"url": "https://github.com/apmantza/pi-free/issues"
|
|
24
|
-
},
|
|
25
|
-
"repository": {
|
|
26
|
-
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/apmantza/pi-free.git"
|
|
28
|
-
},
|
|
29
|
-
"engines": {
|
|
30
|
-
"node": ">=20.0.0"
|
|
31
|
-
},
|
|
32
|
-
"files": [
|
|
33
|
-
"index.ts",
|
|
34
|
-
"providers/**/*.ts",
|
|
35
|
-
"lib/**/*.ts",
|
|
36
|
-
"provider-failover/**/*.ts",
|
|
37
|
-
"config.ts",
|
|
38
|
-
"constants.ts",
|
|
39
|
-
"provider-helper.ts",
|
|
40
|
-
"README.md",
|
|
41
|
-
"LICENSE",
|
|
42
|
-
"CHANGELOG.md",
|
|
43
|
-
"banner.svg",
|
|
44
|
-
"scripts/check-extensions.mjs"
|
|
45
|
-
],
|
|
46
|
-
"scripts": {
|
|
47
|
-
"audit:prod": "npm audit --omit=dev --audit-level=high",
|
|
48
|
-
"check": "node scripts/check-extensions.mjs",
|
|
49
|
-
"check:lockfile": "node scripts/check-lockfile-sync.mjs",
|
|
50
|
-
"check:tarball": "node scripts/check-tarball.mjs",
|
|
51
|
-
"lint": "tsc --noEmit",
|
|
52
|
-
"test": "vitest",
|
|
53
|
-
"test:ui": "vitest --ui",
|
|
54
|
-
"test:run": "vitest run",
|
|
55
|
-
"smoke:cline": "tsx scripts/smoke-cline-xml-bridge.ts",
|
|
56
|
-
"smoke:openmodel": "tsx scripts/smoke-openmodel-wire-format.ts"
|
|
57
|
-
},
|
|
58
|
-
"peerDependencies": {
|
|
59
|
-
"@earendil-works/pi-ai": "
|
|
60
|
-
"@earendil-works/pi-coding-agent": "
|
|
61
|
-
"@earendil-works/pi-tui": "
|
|
62
|
-
},
|
|
63
|
-
"devDependencies": {
|
|
64
|
-
"@vitest/ui": "^4.1.
|
|
65
|
-
"tsx": "^4.
|
|
66
|
-
"typescript": "^6.0.3",
|
|
67
|
-
"vitest": "^4.1.
|
|
68
|
-
},
|
|
69
|
-
"pi": {
|
|
70
|
-
"extensions": [
|
|
71
|
-
"./index.ts"
|
|
72
|
-
]
|
|
73
|
-
}
|
|
74
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-free",
|
|
3
|
+
"version": "2.2.7",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "AI model providers for Pi with free model filtering and dynamic model fetching",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"free-models",
|
|
10
|
+
"paid-models",
|
|
11
|
+
"model-filter",
|
|
12
|
+
"nvidia-nim",
|
|
13
|
+
"kilo",
|
|
14
|
+
"cline",
|
|
15
|
+
"zenmux",
|
|
16
|
+
"crofai",
|
|
17
|
+
"ollama-cloud"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Apostolos Mantzaris",
|
|
21
|
+
"homepage": "https://github.com/apmantza/pi-free#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/apmantza/pi-free/issues"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/apmantza/pi-free.git"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"index.ts",
|
|
34
|
+
"providers/**/*.ts",
|
|
35
|
+
"lib/**/*.ts",
|
|
36
|
+
"provider-failover/**/*.ts",
|
|
37
|
+
"config.ts",
|
|
38
|
+
"constants.ts",
|
|
39
|
+
"provider-helper.ts",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"CHANGELOG.md",
|
|
43
|
+
"banner.svg",
|
|
44
|
+
"scripts/check-extensions.mjs"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"audit:prod": "npm audit --omit=dev --audit-level=high",
|
|
48
|
+
"check": "node scripts/check-extensions.mjs",
|
|
49
|
+
"check:lockfile": "node scripts/check-lockfile-sync.mjs",
|
|
50
|
+
"check:tarball": "node scripts/check-tarball.mjs",
|
|
51
|
+
"lint": "tsc --noEmit",
|
|
52
|
+
"test": "vitest",
|
|
53
|
+
"test:ui": "vitest --ui",
|
|
54
|
+
"test:run": "vitest run",
|
|
55
|
+
"smoke:cline": "tsx scripts/smoke-cline-xml-bridge.ts",
|
|
56
|
+
"smoke:openmodel": "tsx scripts/smoke-openmodel-wire-format.ts"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@earendil-works/pi-ai": ">=0.80.0",
|
|
60
|
+
"@earendil-works/pi-coding-agent": ">=0.80.0",
|
|
61
|
+
"@earendil-works/pi-tui": ">=0.80.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@vitest/ui": "^4.1.10",
|
|
65
|
+
"tsx": "^4.23.0",
|
|
66
|
+
"typescript": "^6.0.3",
|
|
67
|
+
"vitest": "^4.1.10"
|
|
68
|
+
},
|
|
69
|
+
"pi": {
|
|
70
|
+
"extensions": [
|
|
71
|
+
"./index.ts"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnyAPI provider extension.
|
|
3
|
+
*
|
|
4
|
+
* AnyAPI is an OpenAI-compatible gateway with a free plan and a catalog of
|
|
5
|
+
* explicitly free models. It exposes the catalog at /v1/models and routes
|
|
6
|
+
* chat requests through /v1/chat/completions.
|
|
7
|
+
*
|
|
8
|
+
* Setup:
|
|
9
|
+
* ANYAPI_API_KEY=...
|
|
10
|
+
* # or add anyapi_api_key to ~/.pi/free.json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ExtensionAPI,
|
|
15
|
+
ProviderModelConfig,
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import {
|
|
18
|
+
applyHidden,
|
|
19
|
+
getAnyapiApiKey,
|
|
20
|
+
getAnyapiShowPaid,
|
|
21
|
+
} from "../../config.ts";
|
|
22
|
+
import {
|
|
23
|
+
BASE_URL_ANYAPI,
|
|
24
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
25
|
+
PROVIDER_ANYAPI,
|
|
26
|
+
} from "../../constants.ts";
|
|
27
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
28
|
+
import { loadProviderCache } from "../../lib/provider-cache.ts";
|
|
29
|
+
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
30
|
+
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
31
|
+
import { fetchWithRetry, mapOpenRouterModel } from "../../lib/util.ts";
|
|
32
|
+
import {
|
|
33
|
+
createReRegister,
|
|
34
|
+
loadCachedOrFetchModels,
|
|
35
|
+
setupProvider,
|
|
36
|
+
} from "../../provider-helper.ts";
|
|
37
|
+
|
|
38
|
+
const _logger = createLogger("anyapi");
|
|
39
|
+
|
|
40
|
+
interface AnyApiModel {
|
|
41
|
+
id: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
context_length?: number;
|
|
44
|
+
max_completion_tokens?: number | null;
|
|
45
|
+
top_provider?: {
|
|
46
|
+
context_length?: number | null;
|
|
47
|
+
max_completion_tokens?: number | null;
|
|
48
|
+
};
|
|
49
|
+
pricing?: {
|
|
50
|
+
prompt?: string | number | null;
|
|
51
|
+
completion?: string | number | null;
|
|
52
|
+
input_cache_read?: string | number | null;
|
|
53
|
+
input_cache_write?: string | number | null;
|
|
54
|
+
};
|
|
55
|
+
architecture?: {
|
|
56
|
+
input_modalities?: string[] | null;
|
|
57
|
+
output_modalities?: string[] | null;
|
|
58
|
+
};
|
|
59
|
+
supported_parameters?: string[] | null;
|
|
60
|
+
tags?: string[];
|
|
61
|
+
isFree?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ANYAPI_METADATA_VERSION = 1;
|
|
65
|
+
|
|
66
|
+
type AnyApiProviderModel = ProviderModelConfig & {
|
|
67
|
+
_pricingKnown?: boolean;
|
|
68
|
+
_freeKnown?: boolean;
|
|
69
|
+
_isFree?: boolean;
|
|
70
|
+
_anyapiMetadataVersion?: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function hasPricing(model: AnyApiModel): boolean {
|
|
74
|
+
return (
|
|
75
|
+
(model.pricing?.prompt !== null && model.pricing?.prompt !== undefined) ||
|
|
76
|
+
(model.pricing?.completion !== null &&
|
|
77
|
+
model.pricing?.completion !== undefined) ||
|
|
78
|
+
(model.pricing?.input_cache_read !== null &&
|
|
79
|
+
model.pricing?.input_cache_read !== undefined) ||
|
|
80
|
+
(model.pricing?.input_cache_write !== null &&
|
|
81
|
+
model.pricing?.input_cache_write !== undefined)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizePrice(
|
|
86
|
+
value: string | number | null | undefined,
|
|
87
|
+
): string | null {
|
|
88
|
+
return value === null || value === undefined ? null : String(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Detect AnyAPI's explicitly free model labels without treating every model
|
|
93
|
+
* with omitted pricing as free. The API may also expose an authoritative flag
|
|
94
|
+
* or zero pricing, both of which are handled here.
|
|
95
|
+
*/
|
|
96
|
+
export function isAnyApiFreeModel(model: AnyApiModel): boolean {
|
|
97
|
+
if (typeof model.isFree === "boolean") return model.isFree;
|
|
98
|
+
|
|
99
|
+
const label = `${model.id} ${model.name ?? ""}`.toLowerCase();
|
|
100
|
+
if (/\bfree\b/.test(label)) return true;
|
|
101
|
+
|
|
102
|
+
if (!hasPricing(model)) return false;
|
|
103
|
+
const input = Number(model.pricing?.prompt);
|
|
104
|
+
const output = Number(model.pricing?.completion);
|
|
105
|
+
return input === 0 && output === 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function mapAnyApiModel(model: AnyApiModel): AnyApiProviderModel {
|
|
109
|
+
const name = model.name ?? model.id;
|
|
110
|
+
const pricingKnown = hasPricing(model);
|
|
111
|
+
const freeKnown =
|
|
112
|
+
typeof model.isFree === "boolean" ||
|
|
113
|
+
/\bfree\b/i.test(`${model.id} ${name}`) ||
|
|
114
|
+
(pricingKnown && isAnyApiFreeModel(model));
|
|
115
|
+
|
|
116
|
+
const tags = model.tags ?? [];
|
|
117
|
+
const supportsReasoning =
|
|
118
|
+
tags.includes("reasoning") || tags.includes("chat_completions:reasoning");
|
|
119
|
+
const supportsVision =
|
|
120
|
+
tags.includes("vision") || tags.includes("chat_completions:vision");
|
|
121
|
+
|
|
122
|
+
const mapped = mapOpenRouterModel({
|
|
123
|
+
...model,
|
|
124
|
+
name,
|
|
125
|
+
supported_parameters:
|
|
126
|
+
model.supported_parameters ?? (supportsReasoning ? ["reasoning"] : []),
|
|
127
|
+
architecture: model.architecture ?? {
|
|
128
|
+
input_modalities: supportsVision ? ["text", "image"] : ["text"],
|
|
129
|
+
output_modalities: ["text"],
|
|
130
|
+
},
|
|
131
|
+
pricing: model.pricing
|
|
132
|
+
? {
|
|
133
|
+
prompt: normalizePrice(model.pricing.prompt),
|
|
134
|
+
completion: normalizePrice(model.pricing.completion),
|
|
135
|
+
input_cache_read: normalizePrice(model.pricing.input_cache_read),
|
|
136
|
+
input_cache_write: normalizePrice(model.pricing.input_cache_write),
|
|
137
|
+
}
|
|
138
|
+
: undefined,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
...mapped,
|
|
143
|
+
_pricingKnown: pricingKnown,
|
|
144
|
+
...(freeKnown && {
|
|
145
|
+
_freeKnown: true,
|
|
146
|
+
_isFree: isAnyApiFreeModel(model),
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isTextModel(model: AnyApiModel): boolean {
|
|
152
|
+
const outputModalities = model.architecture?.output_modalities ?? [];
|
|
153
|
+
if (outputModalities.length > 0 && !outputModalities.includes("text")) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const tags = model.tags;
|
|
158
|
+
if (!tags || tags.length === 0) return true;
|
|
159
|
+
return tags.some((tag) => tag.startsWith("chat_completions:"));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function fetchAnyApiModels(
|
|
163
|
+
apiKey: string,
|
|
164
|
+
): Promise<AnyApiProviderModel[]> {
|
|
165
|
+
const response = await fetchWithRetry(
|
|
166
|
+
`${BASE_URL_ANYAPI}/models`,
|
|
167
|
+
{
|
|
168
|
+
headers: {
|
|
169
|
+
Authorization: `Bearer ${apiKey}`,
|
|
170
|
+
Accept: "application/json",
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
3,
|
|
175
|
+
1000,
|
|
176
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`AnyAPI API error: ${response.status} ${response.statusText}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const json = (await response.json()) as { data?: AnyApiModel[] };
|
|
186
|
+
const models = (json.data ?? []).flatMap((model) => {
|
|
187
|
+
if (!model.id || !isTextModel(model)) return [];
|
|
188
|
+
return [mapAnyApiModel(model)];
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
_logger.info(`[anyapi] Fetched ${models.length} text models`);
|
|
192
|
+
|
|
193
|
+
// AnyAPI's /models response omits context and output limits for its
|
|
194
|
+
// catalog. Use the global models.dev catalog so canonical model IDs such
|
|
195
|
+
// as qwen/qwen3-coder:free do not fall back to the generic 4096-token
|
|
196
|
+
// defaults in mapOpenRouterModel. The AnyAPI-scoped models.dev entry only
|
|
197
|
+
// contains a small paid subset and does not cover the free catalog.
|
|
198
|
+
const enriched = await safeEnrichModelsWithModelsDev(models);
|
|
199
|
+
return applyHidden(
|
|
200
|
+
enriched.map((model) => ({
|
|
201
|
+
...model,
|
|
202
|
+
_anyapiMetadataVersion: ANYAPI_METADATA_VERSION,
|
|
203
|
+
})),
|
|
204
|
+
PROVIDER_ANYAPI,
|
|
205
|
+
) as AnyApiProviderModel[];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export default async function anyapiProvider(pi: ExtensionAPI) {
|
|
209
|
+
const apiKey = getAnyapiApiKey();
|
|
210
|
+
if (!apiKey) {
|
|
211
|
+
_logger.info("[anyapi] Skipping — ANYAPI_API_KEY not set.");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const cachedModels = loadProviderCache(PROVIDER_ANYAPI) as
|
|
216
|
+
| AnyApiProviderModel[]
|
|
217
|
+
| undefined;
|
|
218
|
+
const needsMetadataMigration = cachedModels?.some(
|
|
219
|
+
(model) => model._anyapiMetadataVersion !== ANYAPI_METADATA_VERSION,
|
|
220
|
+
);
|
|
221
|
+
const allModels = await loadCachedOrFetchModels(
|
|
222
|
+
PROVIDER_ANYAPI,
|
|
223
|
+
() => fetchAnyApiModels(apiKey),
|
|
224
|
+
// Force one refresh for caches written before context metadata was added;
|
|
225
|
+
// subsequent warm startups continue using the normal one-hour cache.
|
|
226
|
+
needsMetadataMigration ? { ttlMs: -1 } : undefined,
|
|
227
|
+
);
|
|
228
|
+
if (allModels.length === 0) {
|
|
229
|
+
_logger.warn("[anyapi] No text models available");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const freeModels = allModels.filter((model) =>
|
|
234
|
+
isFreeModel({ ...model, provider: PROVIDER_ANYAPI }, allModels),
|
|
235
|
+
);
|
|
236
|
+
const stored = { free: freeModels, all: allModels };
|
|
237
|
+
|
|
238
|
+
_logger.info(
|
|
239
|
+
`[anyapi] Registered ${allModels.length} models (${freeModels.length} free)`,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const reRegister = createReRegister(pi, {
|
|
243
|
+
providerId: PROVIDER_ANYAPI,
|
|
244
|
+
baseUrl: BASE_URL_ANYAPI,
|
|
245
|
+
apiKey,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
registerWithGlobalToggle(PROVIDER_ANYAPI, stored, reRegister, true);
|
|
249
|
+
|
|
250
|
+
setupProvider(
|
|
251
|
+
pi,
|
|
252
|
+
{
|
|
253
|
+
providerId: PROVIDER_ANYAPI,
|
|
254
|
+
initialShowPaid: getAnyapiShowPaid(),
|
|
255
|
+
hasKey: true,
|
|
256
|
+
tosUrl: "https://anyapi.ai/terms-of-service",
|
|
257
|
+
reRegister: (models, current) => {
|
|
258
|
+
if (current) {
|
|
259
|
+
stored.free = current.free;
|
|
260
|
+
stored.all = current.all;
|
|
261
|
+
}
|
|
262
|
+
reRegister(models);
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
stored,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const showPaid = getAnyapiShowPaid();
|
|
269
|
+
reRegister(showPaid ? stored.all : stored.free);
|
|
270
|
+
}
|
package/providers/kilo/kilo.ts
CHANGED
|
@@ -226,12 +226,7 @@ export default async function kiloProvider(pi: ExtensionAPI) {
|
|
|
226
226
|
|
|
227
227
|
// Register with global toggle system
|
|
228
228
|
const hasKiloKey = !!kiloApiKey;
|
|
229
|
-
registerWithGlobalToggle(
|
|
230
|
-
PROVIDER_KILO,
|
|
231
|
-
stored,
|
|
232
|
-
reRegister,
|
|
233
|
-
hasKiloKey,
|
|
234
|
-
);
|
|
229
|
+
registerWithGlobalToggle(PROVIDER_KILO, stored, reRegister, hasKiloKey);
|
|
235
230
|
|
|
236
231
|
// OAuth config for Kilo
|
|
237
232
|
const oauthConfig = {
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
AssistantMessageEventStream,
|
|
11
11
|
Context,
|
|
12
12
|
Model,
|
|
13
|
+
ProviderHeaders,
|
|
13
14
|
SimpleStreamOptions,
|
|
14
15
|
} from "@earendil-works/pi-ai";
|
|
15
16
|
import type { ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
@@ -83,8 +84,8 @@ export type OpenCodeSessionTracker = ReturnType<
|
|
|
83
84
|
|
|
84
85
|
export function createOpenCodeHeaders(
|
|
85
86
|
tracker: OpenCodeSessionTracker,
|
|
86
|
-
existingHeaders?:
|
|
87
|
-
):
|
|
87
|
+
existingHeaders?: ProviderHeaders,
|
|
88
|
+
): ProviderHeaders {
|
|
88
89
|
return {
|
|
89
90
|
...existingHeaders,
|
|
90
91
|
...OPENCODE_STATIC_HEADERS,
|
|
@@ -115,17 +116,30 @@ type StreamSimpleFn<TApi extends Api> = (
|
|
|
115
116
|
options?: SimpleStreamOptions,
|
|
116
117
|
) => AssistantMessageEventStream;
|
|
117
118
|
|
|
118
|
-
type
|
|
119
|
-
|
|
119
|
+
type StreamSimpleModule<TApi extends Api> = {
|
|
120
|
+
streamSimple?: StreamSimpleFn<TApi>;
|
|
121
|
+
[key: string]: unknown;
|
|
120
122
|
};
|
|
121
123
|
|
|
122
|
-
type
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
type AnthropicStreamModule = StreamSimpleModule<"anthropic-messages">;
|
|
125
|
+
type OpenAICompletionsStreamModule = StreamSimpleModule<"openai-completions">;
|
|
126
|
+
|
|
127
|
+
function getStreamSimple<TApi extends Api>(
|
|
128
|
+
module: StreamSimpleModule<TApi>,
|
|
129
|
+
legacyExport: string,
|
|
130
|
+
): StreamSimpleFn<TApi> {
|
|
131
|
+
const streamSimple = module.streamSimple ?? module[legacyExport];
|
|
132
|
+
if (typeof streamSimple !== "function") {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Pi AI module does not export ${legacyExport} or streamSimple`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return streamSimple as StreamSimpleFn<TApi>;
|
|
138
|
+
}
|
|
125
139
|
|
|
126
140
|
const piAiSubpathCache = new Map<string, Promise<unknown>>();
|
|
127
141
|
|
|
128
|
-
|
|
142
|
+
function importPiAiSubpath<T>(subpath: string): Promise<T> {
|
|
129
143
|
const specifier = `@earendil-works/pi-ai/${subpath}`;
|
|
130
144
|
const cached = piAiSubpathCache.get(specifier) as Promise<T> | undefined;
|
|
131
145
|
if (cached) return cached;
|
|
@@ -157,6 +171,9 @@ async function importPiAiRootFallback<T>(
|
|
|
157
171
|
): Promise<T | undefined> {
|
|
158
172
|
const subpath = specifier.replace("@earendil-works/pi-ai/", "");
|
|
159
173
|
const requiredExport: Record<string, string> = {
|
|
174
|
+
"api/anthropic-messages": "streamSimpleAnthropic",
|
|
175
|
+
"api/openai-completions": "streamSimpleOpenAICompletions",
|
|
176
|
+
// Keep compatibility with pre-0.80 Pi AI packages.
|
|
160
177
|
anthropic: "streamSimpleAnthropic",
|
|
161
178
|
"openai-completions": "streamSimpleOpenAICompletions",
|
|
162
179
|
};
|
|
@@ -199,6 +216,37 @@ function findPiAiPackageDir(requireBase: string): string | undefined {
|
|
|
199
216
|
return undefined;
|
|
200
217
|
}
|
|
201
218
|
|
|
219
|
+
function resolvePiAiExportTarget(
|
|
220
|
+
exportsMap: Record<string, unknown> | undefined,
|
|
221
|
+
subpath: string,
|
|
222
|
+
): string | undefined {
|
|
223
|
+
if (!exportsMap) return undefined;
|
|
224
|
+
|
|
225
|
+
const getTarget = (entry: unknown): string | undefined => {
|
|
226
|
+
if (typeof entry === "string") return entry;
|
|
227
|
+
if (!entry || typeof entry !== "object") return undefined;
|
|
228
|
+
const conditions = entry as Record<string, unknown>;
|
|
229
|
+
const target = conditions.import ?? conditions.default;
|
|
230
|
+
return typeof target === "string" ? target : undefined;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const exactTarget = getTarget(exportsMap[`./${subpath}`]);
|
|
234
|
+
if (exactTarget) return exactTarget;
|
|
235
|
+
|
|
236
|
+
for (const [pattern, entry] of Object.entries(exportsMap)) {
|
|
237
|
+
if (!pattern.endsWith("/*")) continue;
|
|
238
|
+
const prefix = pattern.slice(2, -1);
|
|
239
|
+
if (subpath.startsWith(prefix)) {
|
|
240
|
+
const target = getTarget(entry);
|
|
241
|
+
if (target) {
|
|
242
|
+
return target.replaceAll("*", subpath.slice(prefix.length));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
202
250
|
function resolvePiAiSubpathFromPackage(specifier: string): string | undefined {
|
|
203
251
|
const subpath = specifier.replace("@earendil-works/pi-ai/", "");
|
|
204
252
|
const candidates = [process.argv[1], import.meta.url].filter(
|
|
@@ -212,11 +260,8 @@ function resolvePiAiSubpathFromPackage(specifier: string): string | undefined {
|
|
|
212
260
|
const pkg = JSON.parse(
|
|
213
261
|
readFileSync(join(pkgDir, "package.json"), "utf-8"),
|
|
214
262
|
);
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
if (typeof targetPath === "string") {
|
|
218
|
-
return join(pkgDir, targetPath);
|
|
219
|
-
}
|
|
263
|
+
const targetPath = resolvePiAiExportTarget(pkg.exports, subpath);
|
|
264
|
+
if (targetPath) return join(pkgDir, targetPath);
|
|
220
265
|
} catch {
|
|
221
266
|
// Try the next resolution base.
|
|
222
267
|
}
|
|
@@ -367,8 +412,12 @@ export function createOpenCodeStreamSimple(
|
|
|
367
412
|
void (async () => {
|
|
368
413
|
try {
|
|
369
414
|
if (isAnthropicOpenCodeEndpoint(model)) {
|
|
370
|
-
const
|
|
371
|
-
await importPiAiSubpath<AnthropicStreamModule>(
|
|
415
|
+
const streamSimpleAnthropic = getStreamSimple(
|
|
416
|
+
await importPiAiSubpath<AnthropicStreamModule>(
|
|
417
|
+
"api/anthropic-messages",
|
|
418
|
+
),
|
|
419
|
+
"streamSimpleAnthropic",
|
|
420
|
+
);
|
|
372
421
|
await pipeStream(
|
|
373
422
|
stream,
|
|
374
423
|
streamSimpleAnthropic(
|
|
@@ -383,10 +432,12 @@ export function createOpenCodeStreamSimple(
|
|
|
383
432
|
return;
|
|
384
433
|
}
|
|
385
434
|
|
|
386
|
-
const
|
|
435
|
+
const streamSimpleOpenAICompletions = getStreamSimple(
|
|
387
436
|
await importPiAiSubpath<OpenAICompletionsStreamModule>(
|
|
388
|
-
"openai-completions",
|
|
389
|
-
)
|
|
437
|
+
"api/openai-completions",
|
|
438
|
+
),
|
|
439
|
+
"streamSimpleOpenAICompletions",
|
|
440
|
+
);
|
|
390
441
|
await pipeStream(
|
|
391
442
|
stream,
|
|
392
443
|
streamSimpleOpenAICompletions(
|
|
@@ -26,10 +26,8 @@ import type {
|
|
|
26
26
|
SimpleStreamOptions,
|
|
27
27
|
ThinkingContent,
|
|
28
28
|
} from "@earendil-works/pi-ai";
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
streamSimpleOpenAICompletions,
|
|
32
|
-
} from "@earendil-works/pi-ai";
|
|
29
|
+
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
30
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/compat";
|
|
33
31
|
import {
|
|
34
32
|
getTokenrouterApiKey,
|
|
35
33
|
getTokenrouterShowPaid,
|
|
@@ -56,6 +54,7 @@ import {
|
|
|
56
54
|
} from "../../provider-helper.ts";
|
|
57
55
|
|
|
58
56
|
const _logger = createLogger("tokenrouter");
|
|
57
|
+
const streamSimpleOpenAICompletions = openAICompletionsApi().streamSimple;
|
|
59
58
|
|
|
60
59
|
// =============================================================================
|
|
61
60
|
// Reasoning cleanup
|
|
@@ -335,7 +334,7 @@ function createTokenRouterOpenAIStream(
|
|
|
335
334
|
): AssistantMessageEventStream {
|
|
336
335
|
const forcePatch = isTokenRouterMinimaxModel(model.id);
|
|
337
336
|
return streamSimpleOpenAICompletions(
|
|
338
|
-
{ ...model, api: "openai-completions" }
|
|
337
|
+
{ ...model, api: "openai-completions" } as Model<"openai-completions">,
|
|
339
338
|
context,
|
|
340
339
|
{
|
|
341
340
|
...options,
|