Merge remote-tracking branch 'origin/main' into add-kimi-deferred-tools
This commit is contained in:
@@ -0,0 +1,112 @@
|
|||||||
|
name: Publish Model Catalog
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- CI
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/publish-model-catalog.yml'
|
||||||
|
- '.gitignore'
|
||||||
|
- 'package.json'
|
||||||
|
- 'packages/ai/**'
|
||||||
|
- 'scripts/publish-model-catalog.mjs'
|
||||||
|
schedule:
|
||||||
|
- cron: '17 */4 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
source_ref:
|
||||||
|
description: 'Commit, branch, or tag to generate from'
|
||||||
|
required: false
|
||||||
|
default: 'main'
|
||||||
|
type: string
|
||||||
|
publish:
|
||||||
|
description: 'Upload the generated catalog to production R2'
|
||||||
|
required: true
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
generate:
|
||||||
|
if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SOURCE_REF: ${{ github.event.workflow_run.head_sha || github.event.pull_request.head.sha || inputs.source_ref || github.sha }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
ref: ${{ env.SOURCE_REF }}
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --ignore-scripts
|
||||||
|
|
||||||
|
- name: Generate model catalog JSON
|
||||||
|
run: npm run generate:model-catalog
|
||||||
|
|
||||||
|
- name: Validate model catalog JSON
|
||||||
|
run: npm run check:model-catalog
|
||||||
|
|
||||||
|
- name: Upload model catalog JSON
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
|
with:
|
||||||
|
name: model-catalog-json
|
||||||
|
path: .artifacts/model-catalog
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
publish:
|
||||||
|
if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_run' || (github.event_name == 'workflow_dispatch' && inputs.publish) }}
|
||||||
|
needs: generate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: pi-model-upload
|
||||||
|
concurrency:
|
||||||
|
group: publish-model-catalog-r2
|
||||||
|
cancel-in-progress: true
|
||||||
|
env:
|
||||||
|
SOURCE_REF: ${{ github.event.workflow_run.head_sha || inputs.source_ref || github.sha }}
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.PI_ARTIFACTS_R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.PI_ARTIFACTS_R2_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: auto
|
||||||
|
AWS_EC2_METADATA_DISABLED: 'true'
|
||||||
|
R2_ENDPOINT: https://67c0d357268b0fca6e0b465bb9d01b84.r2.cloudflarestorage.com
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
ref: ${{ env.SOURCE_REF }}
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Download model catalog JSON
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||||
|
with:
|
||||||
|
name: model-catalog-json
|
||||||
|
path: .artifacts/model-catalog
|
||||||
|
|
||||||
|
- name: Verify AWS CLI
|
||||||
|
run: aws --version
|
||||||
|
|
||||||
|
- name: Publish model catalog to R2
|
||||||
|
run: |
|
||||||
|
node scripts/publish-model-catalog.mjs \
|
||||||
|
--input .artifacts/model-catalog \
|
||||||
|
--bucket pi-artifacts \
|
||||||
|
--endpoint "$R2_ENDPOINT" \
|
||||||
|
--source-commit "$(git rev-parse HEAD)"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
.artifacts/
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|||||||
Generated
+15
-15
@@ -5115,10 +5115,10 @@
|
|||||||
},
|
},
|
||||||
"packages/agent": {
|
"packages/agent": {
|
||||||
"name": "@earendil-works/pi-agent-core",
|
"name": "@earendil-works/pi-agent-core",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-ai": "^0.80.7",
|
"@earendil-works/pi-ai": "^0.80.8",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"typebox": "1.1.38",
|
"typebox": "1.1.38",
|
||||||
"yaml": "2.9.0"
|
"yaml": "2.9.0"
|
||||||
@@ -5467,7 +5467,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ai": {
|
"packages/ai": {
|
||||||
"name": "@earendil-works/pi-ai",
|
"name": "@earendil-works/pi-ai",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "0.91.1",
|
"@anthropic-ai/sdk": "0.91.1",
|
||||||
@@ -5773,12 +5773,12 @@
|
|||||||
},
|
},
|
||||||
"packages/coding-agent": {
|
"packages/coding-agent": {
|
||||||
"name": "@earendil-works/pi-coding-agent",
|
"name": "@earendil-works/pi-coding-agent",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-agent-core": "^0.80.7",
|
"@earendil-works/pi-agent-core": "^0.80.8",
|
||||||
"@earendil-works/pi-ai": "^0.80.7",
|
"@earendil-works/pi-ai": "^0.80.8",
|
||||||
"@earendil-works/pi-tui": "^0.80.7",
|
"@earendil-works/pi-tui": "^0.80.8",
|
||||||
"@silvia-odwyer/photon-node": "0.3.4",
|
"@silvia-odwyer/photon-node": "0.3.4",
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
@@ -5819,32 +5819,32 @@
|
|||||||
},
|
},
|
||||||
"packages/coding-agent/examples/extensions/custom-provider-anthropic": {
|
"packages/coding-agent/examples/extensions/custom-provider-anthropic": {
|
||||||
"name": "pi-extension-custom-provider-anthropic",
|
"name": "pi-extension-custom-provider-anthropic",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "0.52.0"
|
"@anthropic-ai/sdk": "0.52.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": {
|
"packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": {
|
||||||
"name": "pi-extension-custom-provider-gitlab-duo",
|
"name": "pi-extension-custom-provider-gitlab-duo",
|
||||||
"version": "0.80.7"
|
"version": "0.80.8"
|
||||||
},
|
},
|
||||||
"packages/coding-agent/examples/extensions/gondolin": {
|
"packages/coding-agent/examples/extensions/gondolin": {
|
||||||
"name": "pi-extension-gondolin",
|
"name": "pi-extension-gondolin",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/gondolin": "0.12.0"
|
"@earendil-works/gondolin": "0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/coding-agent/examples/extensions/sandbox": {
|
"packages/coding-agent/examples/extensions/sandbox": {
|
||||||
"name": "pi-extension-sandbox",
|
"name": "pi-extension-sandbox",
|
||||||
"version": "1.10.7",
|
"version": "1.10.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sandbox-runtime": "0.0.26"
|
"@anthropic-ai/sandbox-runtime": "0.0.26"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/coding-agent/examples/extensions/with-deps": {
|
"packages/coding-agent/examples/extensions/with-deps": {
|
||||||
"name": "pi-extension-with-deps",
|
"name": "pi-extension-with-deps",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.1.3"
|
"ms": "2.1.3"
|
||||||
},
|
},
|
||||||
@@ -6140,10 +6140,10 @@
|
|||||||
},
|
},
|
||||||
"packages/orchestrator": {
|
"packages/orchestrator": {
|
||||||
"name": "@earendil-works/pi-orchestrator",
|
"name": "@earendil-works/pi-orchestrator",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "^0.80.7"
|
"@earendil-works/pi-coding-agent": "^0.80.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"shx": "0.4.0"
|
"shx": "0.4.0"
|
||||||
@@ -6154,7 +6154,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@earendil-works/pi-tui",
|
"name": "@earendil-works/pi-tui",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-east-asian-width": "1.6.0",
|
"get-east-asian-width": "1.6.0",
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
"check:shrinkwrap": "node scripts/generate-coding-agent-shrinkwrap.mjs --check",
|
"check:shrinkwrap": "node scripts/generate-coding-agent-shrinkwrap.mjs --check",
|
||||||
"check:install-lock:coding-agent": "node scripts/generate-coding-agent-install-lock.mjs --check",
|
"check:install-lock:coding-agent": "node scripts/generate-coding-agent-install-lock.mjs --check",
|
||||||
"check:ts-imports": "node scripts/check-ts-relative-imports.mjs",
|
"check:ts-imports": "node scripts/check-ts-relative-imports.mjs",
|
||||||
|
"generate:model-catalog": "npm --prefix packages/ai run generate-model-catalog",
|
||||||
|
"check:model-catalog": "node scripts/publish-model-catalog.mjs --input .artifacts/model-catalog --dry-run",
|
||||||
"profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui",
|
"profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui",
|
||||||
"profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc",
|
"profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc",
|
||||||
"test": "npm run test --workspaces --if-present",
|
"test": "npm run test --workspaces --if-present",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.80.8] - 2026-07-16
|
||||||
|
|
||||||
## [0.80.7] - 2026-07-14
|
## [0.80.7] - 2026-07-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@earendil-works/pi-agent-core",
|
"name": "@earendil-works/pi-agent-core",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"prepublishOnly": "npm run clean && npm run build"
|
"prepublishOnly": "npm run clean && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-ai": "^0.80.7",
|
"@earendil-works/pi-ai": "^0.80.8",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"typebox": "1.1.38",
|
"typebox": "1.1.38",
|
||||||
"yaml": "2.9.0"
|
"yaml": "2.9.0"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.80.8] - 2026-07-16
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
- Changed runtime authentication to provider-scoped `Models.checkAuth()`, `getAuth()`, `login()`, and `logout()` APIs. `checkAuth()` now returns `AuthCheck | undefined`, and API-key auth resolvers no longer receive a model.
|
- Changed runtime authentication to provider-scoped `Models.checkAuth()`, `getAuth()`, `login()`, and `logout()` APIs. `checkAuth()` now returns `AuthCheck | undefined`, and API-key auth resolvers no longer receive a model.
|
||||||
@@ -17,6 +19,8 @@
|
|||||||
- Added neutral auth-flow information/link events and provider-owned Amazon Bedrock and Google Vertex AI credential selection flows.
|
- Added neutral auth-flow information/link events and provider-owned Amazon Bedrock and Google Vertex AI credential selection flows.
|
||||||
- Added `ModelsStore` with an in-memory default for restoring and persisting dynamic provider catalogs.
|
- Added `ModelsStore` with an in-memory default for restoring and persisting dynamic provider catalogs.
|
||||||
- Added the dynamic Radius `pi-messages` gateway provider with OAuth and credential-specific catalog refresh.
|
- Added the dynamic Radius `pi-messages` gateway provider with OAuth and credential-specific catalog refresh.
|
||||||
|
- Added `Models.refresh({ force: true })` to let providers bypass freshness checks for explicit refreshes.
|
||||||
|
- Added xAI device-code OAuth login and routed Grok 4.5 through OpenAI Responses, with low, medium, and high thinking support ([#6651](https://github.com/earendil-works/pi-mono/pull/6651) by [@Jaaneek](https://github.com/Jaaneek)).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -26,6 +30,7 @@
|
|||||||
|
|
||||||
- Fixed Cloudflare Workers AI and AI Gateway streams to materialize account and gateway endpoint placeholders after auth resolution, including compat streaming with custom model objects.
|
- Fixed Cloudflare Workers AI and AI Gateway streams to materialize account and gateway endpoint placeholders after auth resolution, including compat streaming with custom model objects.
|
||||||
- Fixed lazy provider streams to preserve their final assistant message when forwarding an inner stream.
|
- Fixed lazy provider streams to preserve their final assistant message when forwarding an inner stream.
|
||||||
|
- Fixed OpenAI Codex session IDs longer than 64 characters to meet the API limit ([#6630](https://github.com/earendil-works/pi-mono/issues/6630)).
|
||||||
|
|
||||||
## [0.80.7] - 2026-07-14
|
## [0.80.7] - 2026-07-14
|
||||||
|
|
||||||
|
|||||||
@@ -1047,7 +1047,7 @@ if (result.aborted) console.log('refresh cancelled');
|
|||||||
for (const [provider, error] of result.errors) console.error(provider, error);
|
for (const [provider, error] of result.errors) console.error(provider, error);
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `models.refresh({ allowNetwork: false })` to restore persisted catalogs without network access. Model reads stay synchronous and return the last restored or refreshed list.
|
Use `models.refresh({ allowNetwork: false })` to restore persisted catalogs without network access, or `models.refresh({ force: true })` to bypass provider freshness checks. Model reads stay synchronous and return the last restored or refreshed list.
|
||||||
|
|
||||||
Custom models can carry `headers` (e.g. proxies behind bot detection) and `compat` flags. `Models.getAuth(model)` includes those model headers, and stream methods merge them before explicit request headers and `transformHeaders`. See [OpenAI Compatibility Settings](#openai-compatibility-settings).
|
Custom models can carry `headers` (e.g. proxies behind bot detection) and `compat` flags. `Models.getAuth(model)` includes those model headers, and stream methods merge them before explicit request headers and `transformHeaders`. See [OpenAI Compatibility Settings](#openai-compatibility-settings).
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@earendil-works/pi-ai",
|
"name": "@earendil-works/pi-ai",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
@@ -34,6 +34,10 @@
|
|||||||
"./bedrock-provider": {
|
"./bedrock-provider": {
|
||||||
"types": "./dist/bedrock-provider.d.ts",
|
"types": "./dist/bedrock-provider.d.ts",
|
||||||
"import": "./dist/bedrock-provider.js"
|
"import": "./dist/bedrock-provider.js"
|
||||||
|
},
|
||||||
|
"./bun-oauth": {
|
||||||
|
"types": "./dist/bun-oauth.d.ts",
|
||||||
|
"import": "./dist/bun-oauth.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -46,6 +50,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "shx rm -rf dist",
|
"clean": "shx rm -rf dist",
|
||||||
"generate-models": "node scripts/generate-models.ts",
|
"generate-models": "node scripts/generate-models.ts",
|
||||||
|
"generate-model-catalog": "node scripts/generate-models.ts --strict --json-only --json-output ../../.artifacts/model-catalog",
|
||||||
"generate-image-models": "node scripts/generate-image-models.ts",
|
"generate-image-models": "node scripts/generate-image-models.ts",
|
||||||
"build": "npm run generate-models && npm run generate-image-models && tsgo -p tsconfig.build.json",
|
"build": "npm run generate-models && npm run generate-image-models && tsgo -p tsconfig.build.json",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { readdirSync, rmSync, writeFileSync } from "fs";
|
import { mkdirSync, readdirSync, rmSync, writeFileSync } from "fs";
|
||||||
import { join, dirname } from "path";
|
import { dirname, join, resolve } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import {
|
import {
|
||||||
CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL,
|
CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL,
|
||||||
@@ -22,6 +22,40 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
const packageRoot = join(__dirname, "..");
|
const packageRoot = join(__dirname, "..");
|
||||||
|
|
||||||
|
function readGeneratorOptions(args: string[]): {
|
||||||
|
strict: boolean;
|
||||||
|
jsonOnly: boolean;
|
||||||
|
jsonOutputDir: string | undefined;
|
||||||
|
} {
|
||||||
|
let strict = false;
|
||||||
|
let jsonOnly = false;
|
||||||
|
let jsonOutputDir: string | undefined;
|
||||||
|
|
||||||
|
for (let index = 0; index < args.length; index++) {
|
||||||
|
const arg = args[index];
|
||||||
|
if (arg === "--strict") {
|
||||||
|
strict = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--json-only") {
|
||||||
|
jsonOnly = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--json-output") {
|
||||||
|
const value = args[++index];
|
||||||
|
if (!value) throw new Error("--json-output requires a directory");
|
||||||
|
jsonOutputDir = resolve(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonOnly && !jsonOutputDir) throw new Error("--json-only requires --json-output");
|
||||||
|
return { strict, jsonOnly, jsonOutputDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatorOptions = readGeneratorOptions(process.argv.slice(2));
|
||||||
|
|
||||||
interface ModelsDevModel {
|
interface ModelsDevModel {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -191,6 +225,16 @@ const DEEPSEEK_V4_THINKING_LEVEL_MAP = {
|
|||||||
max: "max",
|
max: "max",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const KIMI_K3_THINKING_LEVEL_MAP = {
|
||||||
|
off: null,
|
||||||
|
minimal: null,
|
||||||
|
low: null,
|
||||||
|
medium: null,
|
||||||
|
high: null,
|
||||||
|
xhigh: null,
|
||||||
|
max: "max",
|
||||||
|
} as const;
|
||||||
|
|
||||||
const ANT_LING_RING_THINKING_LEVEL_MAP = {
|
const ANT_LING_RING_THINKING_LEVEL_MAP = {
|
||||||
off: null,
|
off: null,
|
||||||
minimal: null,
|
minimal: null,
|
||||||
@@ -255,6 +299,14 @@ const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([
|
|||||||
"gpt-5.6-terra",
|
"gpt-5.6-terra",
|
||||||
"gpt-5.6-luna",
|
"gpt-5.6-luna",
|
||||||
]);
|
]);
|
||||||
|
const XAI_RESPONSES_MODEL_ID = "grok-4.5";
|
||||||
|
const XAI_RESPONSES_EFFORT_LEVEL_MAP = {
|
||||||
|
off: null,
|
||||||
|
minimal: null,
|
||||||
|
} as const;
|
||||||
|
const XAI_RESPONSES_COMPAT: OpenAIResponsesCompat = {
|
||||||
|
supportsLongCacheRetention: false,
|
||||||
|
};
|
||||||
|
|
||||||
const OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS = new Set([
|
const OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS = new Set([
|
||||||
"opencode:deepseek-v4-flash",
|
"opencode:deepseek-v4-flash",
|
||||||
@@ -539,6 +591,9 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
|
|||||||
) {
|
) {
|
||||||
mergeThinkingLevelMap(model, { off: "none" });
|
mergeThinkingLevelMap(model, { off: "none" });
|
||||||
}
|
}
|
||||||
|
if (model.provider === "xai" && model.api === "openai-responses" && model.id === XAI_RESPONSES_MODEL_ID) {
|
||||||
|
mergeThinkingLevelMap(model, XAI_RESPONSES_EFFORT_LEVEL_MAP);
|
||||||
|
}
|
||||||
if (supportsOpenAiXhigh(model.id)) {
|
if (supportsOpenAiXhigh(model.id)) {
|
||||||
mergeThinkingLevelMap(model, { xhigh: "xhigh" });
|
mergeThinkingLevelMap(model, { xhigh: "xhigh" });
|
||||||
}
|
}
|
||||||
@@ -678,6 +733,7 @@ async function fetchNvidiaNimModelIds(): Promise<Map<string, string>> {
|
|||||||
try {
|
try {
|
||||||
console.log("Fetching models from NVIDIA NIM API...");
|
console.log("Fetching models from NVIDIA NIM API...");
|
||||||
const response = await fetch(`${NVIDIA_BASE_URL}/models`);
|
const response = await fetch(`${NVIDIA_BASE_URL}/models`);
|
||||||
|
if (!response.ok) throw new Error(`NVIDIA NIM API returned ${response.status}`);
|
||||||
const data = (await response.json()) as { data?: NvidiaNimModelListItem[] };
|
const data = (await response.json()) as { data?: NvidiaNimModelListItem[] };
|
||||||
const modelIds = new Map<string, string>();
|
const modelIds = new Map<string, string>();
|
||||||
|
|
||||||
@@ -690,6 +746,7 @@ async function fetchNvidiaNimModelIds(): Promise<Map<string, string>> {
|
|||||||
return modelIds;
|
return modelIds;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch NVIDIA NIM models:", error);
|
console.error("Failed to fetch NVIDIA NIM models:", error);
|
||||||
|
if (generatorOptions.strict) throw error;
|
||||||
return new Map();
|
return new Map();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,6 +755,7 @@ async function fetchOpenRouterModels(): Promise<Model<any>[]> {
|
|||||||
try {
|
try {
|
||||||
console.log("Fetching models from OpenRouter API...");
|
console.log("Fetching models from OpenRouter API...");
|
||||||
const response = await fetch("https://openrouter.ai/api/v1/models");
|
const response = await fetch("https://openrouter.ai/api/v1/models");
|
||||||
|
if (!response.ok) throw new Error(`OpenRouter API returned ${response.status}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const models: Model<any>[] = [];
|
const models: Model<any>[] = [];
|
||||||
@@ -750,6 +808,7 @@ async function fetchOpenRouterModels(): Promise<Model<any>[]> {
|
|||||||
return models;
|
return models;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch OpenRouter models:", error);
|
console.error("Failed to fetch OpenRouter models:", error);
|
||||||
|
if (generatorOptions.strict) throw error;
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -758,6 +817,7 @@ async function fetchAiGatewayModels(): Promise<Model<any>[]> {
|
|||||||
try {
|
try {
|
||||||
console.log("Fetching models from Vercel AI Gateway API...");
|
console.log("Fetching models from Vercel AI Gateway API...");
|
||||||
const response = await fetch(`${AI_GATEWAY_MODELS_URL}/models`);
|
const response = await fetch(`${AI_GATEWAY_MODELS_URL}/models`);
|
||||||
|
if (!response.ok) throw new Error(`Vercel AI Gateway API returned ${response.status}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const models: Model<any>[] = [];
|
const models: Model<any>[] = [];
|
||||||
|
|
||||||
@@ -808,6 +868,7 @@ async function fetchAiGatewayModels(): Promise<Model<any>[]> {
|
|||||||
return models;
|
return models;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch Vercel AI Gateway models:", error);
|
console.error("Failed to fetch Vercel AI Gateway models:", error);
|
||||||
|
if (generatorOptions.strict) throw error;
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -816,6 +877,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||||||
try {
|
try {
|
||||||
console.log("Fetching models from models.dev API...");
|
console.log("Fetching models from models.dev API...");
|
||||||
const response = await fetch("https://models.dev/api.json");
|
const response = await fetch("https://models.dev/api.json");
|
||||||
|
if (!response.ok) throw new Error(`models.dev API returned ${response.status}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const models: Model<any>[] = [];
|
const models: Model<any>[] = [];
|
||||||
@@ -1127,13 +1189,15 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||||||
for (const [modelId, model] of Object.entries(data.xai.models)) {
|
for (const [modelId, model] of Object.entries(data.xai.models)) {
|
||||||
const m = model as ModelsDevModel;
|
const m = model as ModelsDevModel;
|
||||||
if (m.tool_call !== true) continue;
|
if (m.tool_call !== true) continue;
|
||||||
|
const useResponsesApi = modelId === XAI_RESPONSES_MODEL_ID;
|
||||||
|
|
||||||
models.push({
|
models.push({
|
||||||
id: modelId,
|
id: modelId,
|
||||||
name: m.name || modelId,
|
name: m.name || modelId,
|
||||||
api: "openai-completions",
|
api: useResponsesApi ? "openai-responses" : "openai-completions",
|
||||||
provider: "xai",
|
provider: "xai",
|
||||||
baseUrl: "https://api.x.ai/v1",
|
baseUrl: "https://api.x.ai/v1",
|
||||||
|
...(useResponsesApi ? { compat: { ...XAI_RESPONSES_COMPAT } } : {}),
|
||||||
reasoning: m.reasoning === true,
|
reasoning: m.reasoning === true,
|
||||||
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
|
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
@@ -1610,13 +1674,15 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||||||
for (const [modelId, m] of Object.entries(moonshotModels[key])) {
|
for (const [modelId, m] of Object.entries(moonshotModels[key])) {
|
||||||
if (m.tool_call !== true) continue;
|
if (m.tool_call !== true) continue;
|
||||||
|
|
||||||
|
const isKimiK3 = modelId === "kimi-k3";
|
||||||
models.push({
|
models.push({
|
||||||
id: modelId,
|
id: modelId,
|
||||||
name: m.name || modelId,
|
name: m.name || modelId,
|
||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
provider,
|
provider,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
reasoning: m.reasoning === true,
|
reasoning: isKimiK3 || m.reasoning === true,
|
||||||
|
...(isKimiK3 ? { thinkingLevelMap: KIMI_K3_THINKING_LEVEL_MAP } : {}),
|
||||||
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
|
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: m.cost?.input || 0,
|
input: m.cost?.input || 0,
|
||||||
@@ -1626,7 +1692,9 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||||||
},
|
},
|
||||||
contextWindow: m.limit?.context || 4096,
|
contextWindow: m.limit?.context || 4096,
|
||||||
maxTokens: m.limit?.output || 4096,
|
maxTokens: m.limit?.output || 4096,
|
||||||
compat: moonshotCompat,
|
compat: isKimiK3
|
||||||
|
? { ...moonshotCompat, requiresReasoningContentOnAssistantMessages: true }
|
||||||
|
: moonshotCompat,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1691,6 +1759,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||||||
return models;
|
return models;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load models.dev data:", error);
|
console.error("Failed to load models.dev data:", error);
|
||||||
|
if (generatorOptions.strict) throw error;
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2224,85 +2293,110 @@ async function generateModels() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate TypeScript files: one catalog per provider plus an aggregator
|
const sortedProviderIds = Object.keys(providers).sort();
|
||||||
const generatedHeader = `// This file is auto-generated by scripts/generate-models.ts
|
|
||||||
|
if (!generatorOptions.jsonOnly) {
|
||||||
|
// Generate TypeScript files: one catalog per provider plus an aggregator
|
||||||
|
const generatedHeader = `// This file is auto-generated by scripts/generate-models.ts
|
||||||
// Do not edit manually - run 'npm run generate-models' to update
|
// Do not edit manually - run 'npm run generate-models' to update
|
||||||
|
|
||||||
`;
|
`;
|
||||||
const catalogConstName = (providerId: string) => `${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`;
|
const catalogConstName = (providerId: string) =>
|
||||||
|
`${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`;
|
||||||
|
|
||||||
function emitModel(model: Model<any>, indent: string): string {
|
function emitModel(model: Model<any>, indent: string): string {
|
||||||
let output = `${indent}"${model.id}": {\n`;
|
let output = `${indent}"${model.id}": {\n`;
|
||||||
output += `${indent}\tid: "${model.id}",\n`;
|
output += `${indent}\tid: "${model.id}",\n`;
|
||||||
output += `${indent}\tname: "${model.name}",\n`;
|
output += `${indent}\tname: "${model.name}",\n`;
|
||||||
output += `${indent}\tapi: "${model.api}",\n`;
|
output += `${indent}\tapi: "${model.api}",\n`;
|
||||||
output += `${indent}\tprovider: "${model.provider}",\n`;
|
output += `${indent}\tprovider: "${model.provider}",\n`;
|
||||||
if (model.baseUrl !== undefined) {
|
if (model.baseUrl !== undefined) {
|
||||||
output += `${indent}\tbaseUrl: "${model.baseUrl}",\n`;
|
output += `${indent}\tbaseUrl: "${model.baseUrl}",\n`;
|
||||||
|
}
|
||||||
|
if (model.headers) {
|
||||||
|
output += `${indent}\theaders: ${JSON.stringify(model.headers)},\n`;
|
||||||
|
}
|
||||||
|
if (model.compat) {
|
||||||
|
output += `${indent}\tcompat: ${JSON.stringify(model.compat)},\n`;
|
||||||
|
}
|
||||||
|
output += `${indent}\treasoning: ${model.reasoning},\n`;
|
||||||
|
if (model.thinkingLevelMap) {
|
||||||
|
output += `${indent}\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`;
|
||||||
|
}
|
||||||
|
output += `${indent}\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`;
|
||||||
|
output += `${indent}\tcost: {\n`;
|
||||||
|
output += `${indent}\t\tinput: ${model.cost.input},\n`;
|
||||||
|
output += `${indent}\t\toutput: ${model.cost.output},\n`;
|
||||||
|
output += `${indent}\t\tcacheRead: ${model.cost.cacheRead},\n`;
|
||||||
|
output += `${indent}\t\tcacheWrite: ${model.cost.cacheWrite},\n`;
|
||||||
|
if (model.cost.tiers) {
|
||||||
|
output += `${indent}\t\ttiers: ${JSON.stringify(model.cost.tiers)},\n`;
|
||||||
|
}
|
||||||
|
output += `${indent}\t},\n`;
|
||||||
|
output += `${indent}\tcontextWindow: ${model.contextWindow},\n`;
|
||||||
|
output += `${indent}\tmaxTokens: ${model.maxTokens},\n`;
|
||||||
|
output += `${indent}} satisfies Model<"${model.api}">,\n`;
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
if (model.headers) {
|
|
||||||
output += `${indent}\theaders: ${JSON.stringify(model.headers)},\n`;
|
|
||||||
}
|
|
||||||
if (model.compat) {
|
|
||||||
output += `${indent}\tcompat: ${JSON.stringify(model.compat)},\n`;
|
|
||||||
}
|
|
||||||
output += `${indent}\treasoning: ${model.reasoning},\n`;
|
|
||||||
if (model.thinkingLevelMap) {
|
|
||||||
output += `${indent}\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`;
|
|
||||||
}
|
|
||||||
output += `${indent}\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`;
|
|
||||||
output += `${indent}\tcost: {\n`;
|
|
||||||
output += `${indent}\t\tinput: ${model.cost.input},\n`;
|
|
||||||
output += `${indent}\t\toutput: ${model.cost.output},\n`;
|
|
||||||
output += `${indent}\t\tcacheRead: ${model.cost.cacheRead},\n`;
|
|
||||||
output += `${indent}\t\tcacheWrite: ${model.cost.cacheWrite},\n`;
|
|
||||||
if (model.cost.tiers) {
|
|
||||||
output += `${indent}\t\ttiers: ${JSON.stringify(model.cost.tiers)},\n`;
|
|
||||||
}
|
|
||||||
output += `${indent}\t},\n`;
|
|
||||||
output += `${indent}\tcontextWindow: ${model.contextWindow},\n`;
|
|
||||||
output += `${indent}\tmaxTokens: ${model.maxTokens},\n`;
|
|
||||||
output += `${indent}} satisfies Model<"${model.api}">,\n`;
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedProviderIds = Object.keys(providers).sort();
|
const providersDir = join(packageRoot, "src/providers");
|
||||||
const providersDir = join(packageRoot, "src/providers");
|
|
||||||
|
|
||||||
// Remove stale per-provider catalogs
|
// Remove stale per-provider catalogs
|
||||||
for (const entry of readdirSync(providersDir)) {
|
for (const entry of readdirSync(providersDir)) {
|
||||||
if (entry.endsWith(".models.ts")) {
|
if (entry.endsWith(".models.ts")) {
|
||||||
rmSync(join(providersDir, entry));
|
rmSync(join(providersDir, entry));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Per-provider catalogs (sorted for deterministic output)
|
// Per-provider catalogs (sorted for deterministic output)
|
||||||
for (const providerId of sortedProviderIds) {
|
for (const providerId of sortedProviderIds) {
|
||||||
const models = providers[providerId];
|
const models = providers[providerId];
|
||||||
|
let output = generatedHeader;
|
||||||
|
output += `import type { Model } from "../types.ts";\n\n`;
|
||||||
|
output += `export const ${catalogConstName(providerId)} = {\n`;
|
||||||
|
const sortedModelIds = Object.keys(models).sort();
|
||||||
|
for (const modelId of sortedModelIds) {
|
||||||
|
output += emitModel(models[modelId], "\t");
|
||||||
|
}
|
||||||
|
output += `} as const;\n`;
|
||||||
|
writeFileSync(join(providersDir, `${providerId}.models.ts`), output);
|
||||||
|
}
|
||||||
|
console.log(`Generated ${sortedProviderIds.length} catalogs under src/providers/`);
|
||||||
|
|
||||||
|
// Aggregator
|
||||||
let output = generatedHeader;
|
let output = generatedHeader;
|
||||||
output += `import type { Model } from "../types.ts";\n\n`;
|
for (const providerId of sortedProviderIds) {
|
||||||
output += `export const ${catalogConstName(providerId)} = {\n`;
|
output += `import { ${catalogConstName(providerId)} } from "./providers/${providerId}.models.ts";\n`;
|
||||||
const sortedModelIds = Object.keys(models).sort();
|
}
|
||||||
for (const modelId of sortedModelIds) {
|
output += `\nexport const MODELS = {\n`;
|
||||||
output += emitModel(models[modelId], "\t");
|
for (const providerId of sortedProviderIds) {
|
||||||
|
output += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`;
|
||||||
}
|
}
|
||||||
output += `} as const;\n`;
|
output += `} as const;\n`;
|
||||||
writeFileSync(join(providersDir, `${providerId}.models.ts`), output);
|
writeFileSync(join(packageRoot, "src/models.generated.ts"), output);
|
||||||
|
console.log("Generated src/models.generated.ts");
|
||||||
}
|
}
|
||||||
console.log(`Generated ${sortedProviderIds.length} catalogs under src/providers/`);
|
|
||||||
|
|
||||||
// Aggregator
|
if (generatorOptions.jsonOutputDir) {
|
||||||
let output = generatedHeader;
|
const jsonProviders: Record<string, Record<string, Model<any>>> = {};
|
||||||
for (const providerId of sortedProviderIds) {
|
for (const providerId of sortedProviderIds) {
|
||||||
output += `import { ${catalogConstName(providerId)} } from "./providers/${providerId}.models.ts";\n`;
|
jsonProviders[providerId] = {};
|
||||||
|
for (const modelId of Object.keys(providers[providerId]).sort()) {
|
||||||
|
jsonProviders[providerId][modelId] = providers[providerId][modelId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerOutputDir = join(generatorOptions.jsonOutputDir, "providers");
|
||||||
|
rmSync(generatorOptions.jsonOutputDir, { recursive: true, force: true });
|
||||||
|
mkdirSync(providerOutputDir, { recursive: true });
|
||||||
|
const writeJson = (path: string, value: unknown) => writeFileSync(path, `${JSON.stringify(value)}\n`);
|
||||||
|
writeJson(join(generatorOptions.jsonOutputDir, "models.json"), jsonProviders);
|
||||||
|
writeJson(join(generatorOptions.jsonOutputDir, "providers.json"), sortedProviderIds);
|
||||||
|
for (const providerId of sortedProviderIds) {
|
||||||
|
writeJson(join(providerOutputDir, `${providerId}.json`), jsonProviders[providerId]);
|
||||||
|
}
|
||||||
|
console.log(`Generated JSON model catalog under ${generatorOptions.jsonOutputDir}`);
|
||||||
}
|
}
|
||||||
output += `\nexport const MODELS = {\n`;
|
|
||||||
for (const providerId of sortedProviderIds) {
|
|
||||||
output += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`;
|
|
||||||
}
|
|
||||||
output += `} as const;\n`;
|
|
||||||
writeFileSync(join(packageRoot, "src/models.generated.ts"), output);
|
|
||||||
console.log("Generated src/models.generated.ts");
|
|
||||||
|
|
||||||
// Print statistics
|
// Print statistics
|
||||||
const totalModels = allModels.length;
|
const totalModels = allModels.length;
|
||||||
@@ -2318,4 +2412,7 @@ async function generateModels() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the generator
|
// Run the generator
|
||||||
generateModels().catch(console.error);
|
generateModels().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ function buildParams(model: Model<"openai-responses">, context: Context, options
|
|||||||
effort: (model.thinkingLevelMap?.off ?? "none") as NonNullable<typeof params.reasoning>["effort"],
|
effort: (model.thinkingLevelMap?.off ?? "none") as NonNullable<typeof params.reasoning>["effort"],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (model.provider === "xai") params.include = ["reasoning.encrypted_content"];
|
||||||
}
|
}
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
|
|||||||
@@ -11,18 +11,46 @@ const importOAuthModule = (specifier: string): Promise<unknown> => {
|
|||||||
return import(runtimeSpecifier);
|
return import(runtimeSpecifier);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadAnthropicOAuth = async (): Promise<OAuthAuth> =>
|
type OAuthFlowLoaders = {
|
||||||
((await importOAuthModule("./anthropic.ts")) as { anthropicOAuth: OAuthAuth }).anthropicOAuth;
|
anthropic: () => OAuthAuth | Promise<OAuthAuth>;
|
||||||
|
openaiCodex: () => OAuthAuth | Promise<OAuthAuth>;
|
||||||
|
githubCopilot: () => OAuthAuth | Promise<OAuthAuth>;
|
||||||
|
xai: () => OAuthAuth | Promise<OAuthAuth>;
|
||||||
|
radius: (options: { name: string; gateway: string }) => OAuthAuth | Promise<OAuthAuth>;
|
||||||
|
};
|
||||||
|
|
||||||
export const loadOpenAICodexOAuth = async (): Promise<OAuthAuth> =>
|
let bundledLoaders: OAuthFlowLoaders | undefined;
|
||||||
((await importOAuthModule("./openai-codex.ts")) as { openaiCodexOAuth: OAuthAuth }).openaiCodexOAuth;
|
|
||||||
|
|
||||||
export const loadGitHubCopilotOAuth = async (): Promise<OAuthAuth> =>
|
/** Registers statically bundled OAuth flows for standalone Bun binaries. */
|
||||||
((await importOAuthModule("./github-copilot.ts")) as { githubCopilotOAuth: OAuthAuth }).githubCopilotOAuth;
|
export function registerBundledOAuthFlowLoaders(loaders: OAuthFlowLoaders): void {
|
||||||
|
bundledLoaders = loaders;
|
||||||
|
}
|
||||||
|
|
||||||
export const loadRadiusOAuth = async (options: { name: string; gateway: string }): Promise<OAuthAuth> =>
|
export const loadAnthropicOAuth = async (): Promise<OAuthAuth> => {
|
||||||
(
|
if (bundledLoaders) return bundledLoaders.anthropic();
|
||||||
|
return ((await importOAuthModule("./anthropic.ts")) as { anthropicOAuth: OAuthAuth }).anthropicOAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadOpenAICodexOAuth = async (): Promise<OAuthAuth> => {
|
||||||
|
if (bundledLoaders) return bundledLoaders.openaiCodex();
|
||||||
|
return ((await importOAuthModule("./openai-codex.ts")) as { openaiCodexOAuth: OAuthAuth }).openaiCodexOAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadGitHubCopilotOAuth = async (): Promise<OAuthAuth> => {
|
||||||
|
if (bundledLoaders) return bundledLoaders.githubCopilot();
|
||||||
|
return ((await importOAuthModule("./github-copilot.ts")) as { githubCopilotOAuth: OAuthAuth }).githubCopilotOAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadXaiOAuth = async (): Promise<OAuthAuth> => {
|
||||||
|
if (bundledLoaders) return bundledLoaders.xai();
|
||||||
|
return ((await importOAuthModule("./xai.ts")) as { xaiOAuth: OAuthAuth }).xaiOAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadRadiusOAuth = async (options: { name: string; gateway: string }): Promise<OAuthAuth> => {
|
||||||
|
if (bundledLoaders) return bundledLoaders.radius(options);
|
||||||
|
return (
|
||||||
(await importOAuthModule("./radius.ts")) as {
|
(await importOAuthModule("./radius.ts")) as {
|
||||||
createRadiusOAuth: (input: { name: string; gateway: string }) => OAuthAuth;
|
createRadiusOAuth: (input: { name: string; gateway: string }) => OAuthAuth;
|
||||||
}
|
}
|
||||||
).createRadiusOAuth(options);
|
).createRadiusOAuth(options);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
/**
|
||||||
|
* xAI OAuth device-code flow.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { AuthInteraction, OAuthAuth, OAuthCredential } from "../types.ts";
|
||||||
|
import { pollOAuthDeviceCodeFlow } from "./device-code.ts";
|
||||||
|
|
||||||
|
const XAI_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
||||||
|
const XAI_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
||||||
|
const XAI_DEVICE_CODE_URL = "https://auth.x.ai/oauth2/device/code";
|
||||||
|
const XAI_TOKEN_URL = "https://auth.x.ai/oauth2/token";
|
||||||
|
// Refresh slightly before the reported expiry to avoid using a token that dies mid-request.
|
||||||
|
const REFRESH_SKEW_MS = 5 * 60 * 1000;
|
||||||
|
const DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
|
||||||
|
|
||||||
|
type JsonObject = Record<string, unknown>;
|
||||||
|
|
||||||
|
type OAuthHttpResponse = {
|
||||||
|
ok: boolean;
|
||||||
|
status: number;
|
||||||
|
body: JsonObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
type XaiDeviceCode = {
|
||||||
|
deviceCode: string;
|
||||||
|
userCode: string;
|
||||||
|
verificationUri: string;
|
||||||
|
intervalSeconds?: number;
|
||||||
|
expiresInSeconds: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function requiredString(body: JsonObject, field: string): string {
|
||||||
|
const value = body[field];
|
||||||
|
if (typeof value !== "string" || value.length === 0) {
|
||||||
|
throw new Error(`Invalid xAI OAuth response field: ${field}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function positiveNumber(body: JsonObject, field: string): number {
|
||||||
|
const value = body[field];
|
||||||
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
||||||
|
throw new Error(`Invalid xAI OAuth response field: ${field}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The verification URI is opened in the user's browser; force it to be an https URL
|
||||||
|
// so a malicious response cannot make `open` launch something else.
|
||||||
|
function validateVerificationUri(raw: string): string {
|
||||||
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL(raw);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Untrusted verification URI in xAI OAuth response");
|
||||||
|
}
|
||||||
|
if (url.protocol !== "https:") {
|
||||||
|
throw new Error("Untrusted verification URI in xAI OAuth response");
|
||||||
|
}
|
||||||
|
return url.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postForm(url: string, fields: Record<string, string>, signal?: AbortSignal): Promise<OAuthHttpResponse> {
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams(fields),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
throw new Error("Login cancelled");
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: JsonObject;
|
||||||
|
try {
|
||||||
|
const parsed = (await response.json()) as unknown;
|
||||||
|
body = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as JsonObject) : {};
|
||||||
|
} catch {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
throw new Error("Login cancelled");
|
||||||
|
}
|
||||||
|
throw new Error(`xAI OAuth returned invalid JSON (HTTP ${response.status})`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestFailure(action: string, response: OAuthHttpResponse): Error {
|
||||||
|
const error = typeof response.body.error === "string" ? response.body.error : undefined;
|
||||||
|
const description =
|
||||||
|
typeof response.body.error_description === "string" ? response.body.error_description : undefined;
|
||||||
|
const detail = [error, description].filter(Boolean).join(": ");
|
||||||
|
return new Error(`xAI OAuth ${action} failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDeviceCode(body: JsonObject): XaiDeviceCode {
|
||||||
|
// RFC 8628 allows interval 0 (no minimum wait); fall back to the poller's
|
||||||
|
// default instead of failing on non-positive or malformed values.
|
||||||
|
const interval = body.interval;
|
||||||
|
const intervalSeconds =
|
||||||
|
typeof interval === "number" && Number.isFinite(interval) && interval > 0 ? interval : undefined;
|
||||||
|
return {
|
||||||
|
deviceCode: requiredString(body, "device_code"),
|
||||||
|
userCode: requiredString(body, "user_code"),
|
||||||
|
verificationUri: validateVerificationUri(requiredString(body, "verification_uri")),
|
||||||
|
intervalSeconds,
|
||||||
|
expiresInSeconds: positiveNumber(body, "expires_in"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function credentialsFromTokenResponse(body: JsonObject, previousRefreshToken?: string): OAuthCredential {
|
||||||
|
const access = requiredString(body, "access_token");
|
||||||
|
// xAI may omit refresh_token on refresh when the token is not rotated.
|
||||||
|
const refresh =
|
||||||
|
body.refresh_token === undefined && previousRefreshToken
|
||||||
|
? previousRefreshToken
|
||||||
|
: requiredString(body, "refresh_token");
|
||||||
|
const expiresInSeconds =
|
||||||
|
body.expires_in === undefined ? DEFAULT_TOKEN_LIFETIME_SECONDS : positiveNumber(body, "expires_in");
|
||||||
|
return {
|
||||||
|
type: "oauth",
|
||||||
|
access,
|
||||||
|
refresh,
|
||||||
|
expires: Date.now() + expiresInSeconds * 1000 - REFRESH_SKEW_MS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestDeviceCode(signal?: AbortSignal): Promise<XaiDeviceCode> {
|
||||||
|
const response = await postForm(
|
||||||
|
XAI_DEVICE_CODE_URL,
|
||||||
|
{
|
||||||
|
client_id: XAI_CLIENT_ID,
|
||||||
|
scope: XAI_SCOPE,
|
||||||
|
referrer: "pi",
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw requestFailure("device authorization", response);
|
||||||
|
}
|
||||||
|
return parseDeviceCode(response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollForTokens(device: XaiDeviceCode, signal?: AbortSignal): Promise<OAuthCredential> {
|
||||||
|
return pollOAuthDeviceCodeFlow<OAuthCredential>({
|
||||||
|
intervalSeconds: device.intervalSeconds,
|
||||||
|
expiresInSeconds: device.expiresInSeconds,
|
||||||
|
waitBeforeFirstPoll: true,
|
||||||
|
signal,
|
||||||
|
poll: async () => {
|
||||||
|
const response = await postForm(
|
||||||
|
XAI_TOKEN_URL,
|
||||||
|
{
|
||||||
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||||
|
client_id: XAI_CLIENT_ID,
|
||||||
|
device_code: device.deviceCode,
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return { status: "complete", value: credentialsFromTokenResponse(response.body) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = response.body.error;
|
||||||
|
if (error === "authorization_pending") {
|
||||||
|
return { status: "pending" };
|
||||||
|
}
|
||||||
|
if (error === "slow_down") {
|
||||||
|
const interval = response.body.interval;
|
||||||
|
return { status: "slow_down", intervalSeconds: typeof interval === "number" ? interval : undefined };
|
||||||
|
}
|
||||||
|
if (error === "access_denied" || error === "authorization_denied") {
|
||||||
|
return { status: "failed", message: "xAI device authorization was denied" };
|
||||||
|
}
|
||||||
|
if (error === "expired_token") {
|
||||||
|
return { status: "failed", message: "xAI device code expired" };
|
||||||
|
}
|
||||||
|
return { status: "failed", message: requestFailure("device token polling", response).message };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginXai(interaction: AuthInteraction): Promise<OAuthCredential> {
|
||||||
|
const device = await requestDeviceCode(interaction.signal);
|
||||||
|
interaction.notify({
|
||||||
|
type: "device_code",
|
||||||
|
userCode: device.userCode,
|
||||||
|
verificationUri: device.verificationUri,
|
||||||
|
intervalSeconds: device.intervalSeconds,
|
||||||
|
expiresInSeconds: device.expiresInSeconds,
|
||||||
|
});
|
||||||
|
return pollForTokens(device, interaction.signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshXaiToken(refreshToken: string, signal?: AbortSignal): Promise<OAuthCredential> {
|
||||||
|
const response = await postForm(
|
||||||
|
XAI_TOKEN_URL,
|
||||||
|
{
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
client_id: XAI_CLIENT_ID,
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw requestFailure("token refresh", response);
|
||||||
|
}
|
||||||
|
return credentialsFromTokenResponse(response.body, refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const xaiOAuth: OAuthAuth = {
|
||||||
|
name: "xAI (Grok/X subscription)",
|
||||||
|
login: loginXai,
|
||||||
|
refresh: (credential, signal) => refreshXaiToken(credential.refresh, signal),
|
||||||
|
|
||||||
|
async toAuth(credential) {
|
||||||
|
return { apiKey: credential.access };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { anthropicOAuth } from "./auth/oauth/anthropic.ts";
|
||||||
|
import { githubCopilotOAuth } from "./auth/oauth/github-copilot.ts";
|
||||||
|
import { registerBundledOAuthFlowLoaders } from "./auth/oauth/load.ts";
|
||||||
|
import { openaiCodexOAuth } from "./auth/oauth/openai-codex.ts";
|
||||||
|
import { createRadiusOAuth } from "./auth/oauth/radius.ts";
|
||||||
|
import { xaiOAuth } from "./auth/oauth/xai.ts";
|
||||||
|
|
||||||
|
/** Register OAuth flows statically embedded in the standalone Bun binary. */
|
||||||
|
export function registerBunOAuthFlows(): void {
|
||||||
|
registerBundledOAuthFlowLoaders({
|
||||||
|
anthropic: () => anthropicOAuth,
|
||||||
|
openaiCodex: () => openaiCodexOAuth,
|
||||||
|
githubCopilot: () => githubCopilotOAuth,
|
||||||
|
xai: () => xaiOAuth,
|
||||||
|
radius: createRadiusOAuth,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -38,11 +38,15 @@ export interface RefreshModelsContext {
|
|||||||
store: ProviderModelsStore;
|
store: ProviderModelsStore;
|
||||||
/** False during offline/cache-only initialization. */
|
/** False during offline/cache-only initialization. */
|
||||||
allowNetwork: boolean;
|
allowNetwork: boolean;
|
||||||
|
/** Bypass provider freshness checks and fetch immediately when network access is allowed. */
|
||||||
|
force?: boolean;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelsRefreshOptions {
|
export interface ModelsRefreshOptions {
|
||||||
allowNetwork?: boolean;
|
allowNetwork?: boolean;
|
||||||
|
/** Bypass provider freshness checks and fetch immediately when network access is allowed. */
|
||||||
|
force?: boolean;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +294,13 @@ class ModelsImpl implements MutableModels {
|
|||||||
stored = await this.readCredential(provider.id);
|
stored = await this.readCredential(provider.id);
|
||||||
const credential = await this.resolveRefreshCredential(provider, stored, allowNetwork, options.signal);
|
const credential = await this.resolveRefreshCredential(provider, stored, allowNetwork, options.signal);
|
||||||
if (!credential) return;
|
if (!credential) return;
|
||||||
await provider.refreshModels({ credential, store, allowNetwork, signal: options.signal });
|
await provider.refreshModels({
|
||||||
|
credential,
|
||||||
|
store,
|
||||||
|
allowNetwork,
|
||||||
|
force: options.force,
|
||||||
|
signal: options.signal,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!options.signal?.aborted) {
|
if (!options.signal?.aborted) {
|
||||||
errors.set(
|
errors.set(
|
||||||
|
|||||||
@@ -22,6 +22,24 @@ export const KIMI_CODING_MODELS = {
|
|||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
maxTokens: 32768,
|
maxTokens: 32768,
|
||||||
} satisfies Model<"anthropic-messages">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
|
"k3": {
|
||||||
|
id: "k3",
|
||||||
|
name: "Kimi K3",
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "kimi-coding",
|
||||||
|
baseUrl: "https://api.kimi.com/coding",
|
||||||
|
headers: {"User-Agent":"KimiCLI/1.5"},
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1048576,
|
||||||
|
maxTokens: 131072,
|
||||||
|
} satisfies Model<"anthropic-messages">,
|
||||||
"kimi-for-coding": {
|
"kimi-for-coding": {
|
||||||
id: "kimi-for-coding",
|
id: "kimi-for-coding",
|
||||||
name: "Kimi For Coding",
|
name: "Kimi For Coding",
|
||||||
@@ -40,6 +58,24 @@ export const KIMI_CODING_MODELS = {
|
|||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
maxTokens: 32768,
|
maxTokens: 32768,
|
||||||
} satisfies Model<"anthropic-messages">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
|
"kimi-for-coding-highspeed": {
|
||||||
|
id: "kimi-for-coding-highspeed",
|
||||||
|
name: "Kimi For Coding HighSpeed",
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "kimi-coding",
|
||||||
|
baseUrl: "https://api.kimi.com/coding",
|
||||||
|
headers: {"User-Agent":"KimiCLI/1.5"},
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 262144,
|
||||||
|
maxTokens: 32768,
|
||||||
|
} satisfies Model<"anthropic-messages">,
|
||||||
"kimi-k2-thinking": {
|
"kimi-k2-thinking": {
|
||||||
id: "kimi-k2-thinking",
|
id: "kimi-k2-thinking",
|
||||||
name: "Kimi K2 Thinking",
|
name: "Kimi K2 Thinking",
|
||||||
|
|||||||
@@ -168,4 +168,23 @@ export const MOONSHOTAI_CN_MODELS = {
|
|||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
maxTokens: 262144,
|
maxTokens: 262144,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"kimi-k3": {
|
||||||
|
id: "kimi-k3",
|
||||||
|
name: "Kimi K3",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "moonshotai-cn",
|
||||||
|
baseUrl: "https://api.moonshot.cn/v1",
|
||||||
|
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek","requiresReasoningContentOnAssistantMessages":true},
|
||||||
|
reasoning: true,
|
||||||
|
thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":null,"xhigh":null,"max":"max"},
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1048576,
|
||||||
|
maxTokens: 131072,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -168,4 +168,23 @@ export const MOONSHOTAI_MODELS = {
|
|||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
maxTokens: 262144,
|
maxTokens: 262144,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"kimi-k3": {
|
||||||
|
id: "kimi-k3",
|
||||||
|
name: "Kimi K3",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "moonshotai",
|
||||||
|
baseUrl: "https://api.moonshot.ai/v1",
|
||||||
|
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek","requiresReasoningContentOnAssistantMessages":true},
|
||||||
|
reasoning: true,
|
||||||
|
thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":null,"xhigh":null,"max":"max"},
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1048576,
|
||||||
|
maxTokens: 131072,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ export const OPENROUTER_MODELS = {
|
|||||||
cacheRead: 0.3,
|
cacheRead: 0.3,
|
||||||
cacheWrite: 3.75,
|
cacheWrite: 3.75,
|
||||||
},
|
},
|
||||||
contextWindow: 1000000,
|
contextWindow: 200000,
|
||||||
maxTokens: 64000,
|
maxTokens: 64000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"anthropic/claude-sonnet-4.5": {
|
"anthropic/claude-sonnet-4.5": {
|
||||||
@@ -652,13 +652,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.24,
|
input: 0.27,
|
||||||
output: 0.9,
|
output: 1.12,
|
||||||
cacheRead: 0.135,
|
cacheRead: 0.135,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 163840,
|
contextWindow: 163840,
|
||||||
maxTokens: 16384,
|
maxTokens: 65536,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"deepseek/deepseek-chat-v3.1": {
|
"deepseek/deepseek-chat-v3.1": {
|
||||||
id: "deepseek/deepseek-chat-v3.1",
|
id: "deepseek/deepseek-chat-v3.1",
|
||||||
@@ -725,11 +725,11 @@ export const OPENROUTER_MODELS = {
|
|||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.27,
|
input: 0.27,
|
||||||
output: 0.95,
|
output: 1,
|
||||||
cacheRead: 0.13,
|
cacheRead: 0.135,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 163840,
|
contextWindow: 131072,
|
||||||
maxTokens: 32768,
|
maxTokens: 32768,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"deepseek/deepseek-v3.2": {
|
"deepseek/deepseek-v3.2": {
|
||||||
@@ -742,13 +742,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.2145,
|
input: 0.269,
|
||||||
output: 0.32175,
|
output: 0.4,
|
||||||
cacheRead: 0.02145,
|
cacheRead: 0.1345,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 163840,
|
||||||
maxTokens: 64000,
|
maxTokens: 65536,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"deepseek/deepseek-v3.2-exp": {
|
"deepseek/deepseek-v3.2-exp": {
|
||||||
id: "deepseek/deepseek-v3.2-exp",
|
id: "deepseek/deepseek-v3.2-exp",
|
||||||
@@ -779,13 +779,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","max":null,"xhigh":"xhigh"},
|
thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","max":null,"xhigh":"xhigh"},
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.09,
|
input: 0.098,
|
||||||
output: 0.18,
|
output: 0.196,
|
||||||
cacheRead: 0.018,
|
cacheRead: 0.02,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 1048576,
|
contextWindow: 1048575,
|
||||||
maxTokens: 65536,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"deepseek/deepseek-v4-pro": {
|
"deepseek/deepseek-v4-pro": {
|
||||||
id: "deepseek/deepseek-v4-pro",
|
id: "deepseek/deepseek-v4-pro",
|
||||||
@@ -1051,12 +1051,12 @@ export const OPENROUTER_MODELS = {
|
|||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.08,
|
input: 0.08,
|
||||||
output: 0.16,
|
output: 0.45,
|
||||||
cacheRead: 0,
|
cacheRead: 0.04,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 131072,
|
contextWindow: 131072,
|
||||||
maxTokens: 16384,
|
maxTokens: 131072,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"google/gemma-4-26b-a4b-it": {
|
"google/gemma-4-26b-a4b-it": {
|
||||||
id: "google/gemma-4-26b-a4b-it",
|
id: "google/gemma-4-26b-a4b-it",
|
||||||
@@ -1068,13 +1068,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.06,
|
input: 0.1,
|
||||||
output: 0.33,
|
output: 0.3,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 256000,
|
||||||
maxTokens: 4096,
|
maxTokens: 256000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"google/gemma-4-26b-a4b-it:free": {
|
"google/gemma-4-26b-a4b-it:free": {
|
||||||
id: "google/gemma-4-26b-a4b-it:free",
|
id: "google/gemma-4-26b-a4b-it:free",
|
||||||
@@ -1104,13 +1104,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.06,
|
input: 0.22,
|
||||||
output: 0.35,
|
output: 0.55,
|
||||||
cacheRead: 0,
|
cacheRead: 0.12,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
maxTokens: 8192,
|
maxTokens: 262144,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"google/gemma-4-31b-it:free": {
|
"google/gemma-4-31b-it:free": {
|
||||||
id: "google/gemma-4-31b-it:free",
|
id: "google/gemma-4-31b-it:free",
|
||||||
@@ -1303,13 +1303,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.02,
|
input: 0.05,
|
||||||
output: 0.03,
|
output: 0.08,
|
||||||
cacheRead: 0,
|
cacheRead: 0.025,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 131072,
|
contextWindow: 131072,
|
||||||
maxTokens: 16384,
|
maxTokens: 131072,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"meta-llama/llama-3.3-70b-instruct": {
|
"meta-llama/llama-3.3-70b-instruct": {
|
||||||
id: "meta-llama/llama-3.3-70b-instruct",
|
id: "meta-llama/llama-3.3-70b-instruct",
|
||||||
@@ -1321,13 +1321,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.1,
|
input: 0.13,
|
||||||
output: 0.32,
|
output: 0.4,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 131072,
|
contextWindow: 131072,
|
||||||
maxTokens: 16384,
|
maxTokens: 128000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"meta-llama/llama-3.3-70b-instruct:free": {
|
"meta-llama/llama-3.3-70b-instruct:free": {
|
||||||
id: "meta-llama/llama-3.3-70b-instruct:free",
|
id: "meta-llama/llama-3.3-70b-instruct:free",
|
||||||
@@ -1383,6 +1383,24 @@ export const OPENROUTER_MODELS = {
|
|||||||
contextWindow: 327680,
|
contextWindow: 327680,
|
||||||
maxTokens: 16384,
|
maxTokens: 16384,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"meta/muse-spark-1.1": {
|
||||||
|
id: "meta/muse-spark-1.1",
|
||||||
|
name: "Meta: Muse Spark 1.1",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"},
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 1.25,
|
||||||
|
output: 4.25,
|
||||||
|
cacheRead: 0.15,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1048576,
|
||||||
|
maxTokens: 4096,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
"minimax/minimax-m1": {
|
"minimax/minimax-m1": {
|
||||||
id: "minimax/minimax-m1",
|
id: "minimax/minimax-m1",
|
||||||
name: "MiniMax: MiniMax M1",
|
name: "MiniMax: MiniMax M1",
|
||||||
@@ -1393,7 +1411,7 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.4,
|
input: 0.55,
|
||||||
output: 2.2,
|
output: 2.2,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
@@ -1465,13 +1483,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.24,
|
input: 0.3,
|
||||||
output: 0.96,
|
output: 1.2,
|
||||||
cacheRead: 0,
|
cacheRead: 0.06,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 196608,
|
contextWindow: 204800,
|
||||||
maxTokens: 196608,
|
maxTokens: 131072,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"minimax/minimax-m3": {
|
"minimax/minimax-m3": {
|
||||||
id: "minimax/minimax-m3",
|
id: "minimax/minimax-m3",
|
||||||
@@ -1488,8 +1506,8 @@ export const OPENROUTER_MODELS = {
|
|||||||
cacheRead: 0.06,
|
cacheRead: 0.06,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 1000000,
|
contextWindow: 524288,
|
||||||
maxTokens: 131072,
|
maxTokens: 512000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"mistralai/codestral-2508": {
|
"mistralai/codestral-2508": {
|
||||||
id: "mistralai/codestral-2508",
|
id: "mistralai/codestral-2508",
|
||||||
@@ -1700,12 +1718,12 @@ export const OPENROUTER_MODELS = {
|
|||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.02,
|
input: 0.02,
|
||||||
output: 0.03,
|
output: 0.04,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 131072,
|
contextWindow: 131072,
|
||||||
maxTokens: 4096,
|
maxTokens: 16384,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"mistralai/mistral-saba": {
|
"mistralai/mistral-saba": {
|
||||||
id: "mistralai/mistral-saba",
|
id: "mistralai/mistral-saba",
|
||||||
@@ -1753,13 +1771,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.075,
|
input: 0.1,
|
||||||
output: 0.2,
|
output: 0.3,
|
||||||
cacheRead: 0,
|
cacheRead: 0.01,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 131072,
|
||||||
maxTokens: 16384,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"mistralai/mixtral-8x22b-instruct": {
|
"mistralai/mixtral-8x22b-instruct": {
|
||||||
id: "mistralai/mixtral-8x22b-instruct",
|
id: "mistralai/mixtral-8x22b-instruct",
|
||||||
@@ -1845,11 +1863,11 @@ export const OPENROUTER_MODELS = {
|
|||||||
cost: {
|
cost: {
|
||||||
input: 0.6,
|
input: 0.6,
|
||||||
output: 2.5,
|
output: 2.5,
|
||||||
cacheRead: 0.15,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
maxTokens: 100352,
|
maxTokens: 262144,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"moonshotai/kimi-k2.5": {
|
"moonshotai/kimi-k2.5": {
|
||||||
id: "moonshotai/kimi-k2.5",
|
id: "moonshotai/kimi-k2.5",
|
||||||
@@ -1879,13 +1897,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.66,
|
input: 0.95,
|
||||||
output: 3.41,
|
output: 4,
|
||||||
cacheRead: 0.15,
|
cacheRead: 0.16,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
maxTokens: 262144,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"moonshotai/kimi-k2.7-code": {
|
"moonshotai/kimi-k2.7-code": {
|
||||||
id: "moonshotai/kimi-k2.7-code",
|
id: "moonshotai/kimi-k2.7-code",
|
||||||
@@ -1897,9 +1915,9 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.719,
|
input: 0.75,
|
||||||
output: 3.49,
|
output: 3.5,
|
||||||
cacheRead: 0.149,
|
cacheRead: 0.16,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
@@ -2023,12 +2041,12 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.08,
|
input: 0.21,
|
||||||
output: 0.45,
|
output: 0.455,
|
||||||
cacheRead: 0,
|
cacheRead: 0.06,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 1000000,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"nvidia/nemotron-3-super-120b-a12b:free": {
|
"nvidia/nemotron-3-super-120b-a12b:free": {
|
||||||
@@ -2059,13 +2077,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.5,
|
input: 0.6,
|
||||||
output: 2.2,
|
output: 3.6,
|
||||||
cacheRead: 0.1,
|
cacheRead: 0.2,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 512288,
|
||||||
maxTokens: 16384,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"nvidia/nemotron-3-ultra-550b-a55b:free": {
|
"nvidia/nemotron-3-ultra-550b-a55b:free": {
|
||||||
id: "nvidia/nemotron-3-ultra-550b-a55b:free",
|
id: "nvidia/nemotron-3-ultra-550b-a55b:free",
|
||||||
@@ -2245,7 +2263,7 @@ export const OPENROUTER_MODELS = {
|
|||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 1047576,
|
contextWindow: 1047576,
|
||||||
maxTokens: 4096,
|
maxTokens: 32768,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"openai/gpt-4.1-mini": {
|
"openai/gpt-4.1-mini": {
|
||||||
id: "openai/gpt-4.1-mini",
|
id: "openai/gpt-4.1-mini",
|
||||||
@@ -2295,7 +2313,7 @@ export const OPENROUTER_MODELS = {
|
|||||||
cost: {
|
cost: {
|
||||||
input: 2.5,
|
input: 2.5,
|
||||||
output: 10,
|
output: 10,
|
||||||
cacheRead: 0,
|
cacheRead: 1.25,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
@@ -2511,11 +2529,11 @@ export const OPENROUTER_MODELS = {
|
|||||||
cost: {
|
cost: {
|
||||||
input: 1.25,
|
input: 1.25,
|
||||||
output: 10,
|
output: 10,
|
||||||
cacheRead: 0.13,
|
cacheRead: 0.125,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 32000,
|
maxTokens: 16384,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"openai/gpt-5.1-codex": {
|
"openai/gpt-5.1-codex": {
|
||||||
id: "openai/gpt-5.1-codex",
|
id: "openai/gpt-5.1-codex",
|
||||||
@@ -2529,7 +2547,7 @@ export const OPENROUTER_MODELS = {
|
|||||||
cost: {
|
cost: {
|
||||||
input: 1.25,
|
input: 1.25,
|
||||||
output: 10,
|
output: 10,
|
||||||
cacheRead: 0.13,
|
cacheRead: 0.125,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 400000,
|
contextWindow: 400000,
|
||||||
@@ -2977,8 +2995,8 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.03,
|
input: 0.037,
|
||||||
output: 0.15,
|
output: 0.17,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
@@ -2995,13 +3013,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.029,
|
input: 0.03,
|
||||||
output: 0.14,
|
output: 0.13,
|
||||||
cacheRead: 0,
|
cacheRead: 0.03,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 131072,
|
contextWindow: 131072,
|
||||||
maxTokens: 4096,
|
maxTokens: 131072,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"openai/gpt-oss-20b:free": {
|
"openai/gpt-oss-20b:free": {
|
||||||
id: "openai/gpt-oss-20b:free",
|
id: "openai/gpt-oss-20b:free",
|
||||||
@@ -3517,13 +3535,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.04815,
|
input: 0.1,
|
||||||
output: 0.19305,
|
output: 0.3,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 262144,
|
||||||
maxTokens: 32000,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"qwen/qwen3-30b-a3b-thinking-2507": {
|
"qwen/qwen3-30b-a3b-thinking-2507": {
|
||||||
id: "qwen/qwen3-30b-a3b-thinking-2507",
|
id: "qwen/qwen3-30b-a3b-thinking-2507",
|
||||||
@@ -3589,9 +3607,9 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.22,
|
input: 0.3,
|
||||||
output: 1.8,
|
output: 1,
|
||||||
cacheRead: 0,
|
cacheRead: 0.1,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
@@ -3733,13 +3751,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.09,
|
input: 0.1,
|
||||||
output: 1.1,
|
output: 1.1,
|
||||||
cacheRead: 0,
|
cacheRead: 0.07,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
maxTokens: 16384,
|
maxTokens: 262144,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"qwen/qwen3-next-80b-a3b-instruct:free": {
|
"qwen/qwen3-next-80b-a3b-instruct:free": {
|
||||||
id: "qwen/qwen3-next-80b-a3b-instruct:free",
|
id: "qwen/qwen3-next-80b-a3b-instruct:free",
|
||||||
@@ -3787,13 +3805,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.2,
|
input: 0.21,
|
||||||
output: 0.88,
|
output: 1.9,
|
||||||
cacheRead: 0.11,
|
cacheRead: 0.1,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 131072,
|
||||||
maxTokens: 16384,
|
maxTokens: 32768,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"qwen/qwen3-vl-235b-a22b-thinking": {
|
"qwen/qwen3-vl-235b-a22b-thinking": {
|
||||||
id: "qwen/qwen3-vl-235b-a22b-thinking",
|
id: "qwen/qwen3-vl-235b-a22b-thinking",
|
||||||
@@ -3919,7 +3937,7 @@ export const OPENROUTER_MODELS = {
|
|||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
maxTokens: 262144,
|
maxTokens: 65536,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"qwen/qwen3.5-27b": {
|
"qwen/qwen3.5-27b": {
|
||||||
id: "qwen/qwen3.5-27b",
|
id: "qwen/qwen3.5-27b",
|
||||||
@@ -3951,11 +3969,11 @@ export const OPENROUTER_MODELS = {
|
|||||||
cost: {
|
cost: {
|
||||||
input: 0.14,
|
input: 0.14,
|
||||||
output: 1,
|
output: 1,
|
||||||
cacheRead: 0.05,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
maxTokens: 81920,
|
maxTokens: 262144,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"qwen/qwen3.5-397b-a17b": {
|
"qwen/qwen3.5-397b-a17b": {
|
||||||
id: "qwen/qwen3.5-397b-a17b",
|
id: "qwen/qwen3.5-397b-a17b",
|
||||||
@@ -3967,13 +3985,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.385,
|
input: 0.45,
|
||||||
output: 2.45,
|
output: 3,
|
||||||
cacheRead: 0.111,
|
cacheRead: 0.225,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 131072,
|
contextWindow: 262144,
|
||||||
maxTokens: 4096,
|
maxTokens: 65536,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"qwen/qwen3.5-9b": {
|
"qwen/qwen3.5-9b": {
|
||||||
id: "qwen/qwen3.5-9b",
|
id: "qwen/qwen3.5-9b",
|
||||||
@@ -4057,13 +4075,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.289,
|
input: 0.45,
|
||||||
output: 2.4,
|
output: 2.7,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 131072,
|
contextWindow: 262144,
|
||||||
maxTokens: 131072,
|
maxTokens: 65536,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"qwen/qwen3.6-35b-a3b": {
|
"qwen/qwen3.6-35b-a3b": {
|
||||||
id: "qwen/qwen3.6-35b-a3b",
|
id: "qwen/qwen3.6-35b-a3b",
|
||||||
@@ -4147,10 +4165,10 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 1.25,
|
input: 1.475,
|
||||||
output: 3.75,
|
output: 4.425,
|
||||||
cacheRead: 0.25,
|
cacheRead: 0.295,
|
||||||
cacheWrite: 1.5625,
|
cacheWrite: 1.84375,
|
||||||
},
|
},
|
||||||
contextWindow: 1000000,
|
contextWindow: 1000000,
|
||||||
maxTokens: 65536,
|
maxTokens: 65536,
|
||||||
@@ -4291,13 +4309,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.14,
|
input: 0.2,
|
||||||
output: 0.58,
|
output: 0.8,
|
||||||
cacheRead: 0.035,
|
cacheRead: 0.05,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 262144,
|
||||||
maxTokens: 4096,
|
maxTokens: 131072,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"tencent/hy3-preview": {
|
"tencent/hy3-preview": {
|
||||||
id: "tencent/hy3-preview",
|
id: "tencent/hy3-preview",
|
||||||
@@ -4453,13 +4471,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.105,
|
input: 0.14,
|
||||||
output: 0.28,
|
output: 0.28,
|
||||||
cacheRead: 0.028,
|
cacheRead: 0.0028,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 1048576,
|
||||||
maxTokens: 4096,
|
maxTokens: 131072,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"xiaomi/mimo-v2.5-pro": {
|
"xiaomi/mimo-v2.5-pro": {
|
||||||
id: "xiaomi/mimo-v2.5-pro",
|
id: "xiaomi/mimo-v2.5-pro",
|
||||||
@@ -4543,13 +4561,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.43,
|
input: 0.5,
|
||||||
output: 1.75,
|
output: 2,
|
||||||
cacheRead: 0.08,
|
cacheRead: 0.1,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 198000,
|
contextWindow: 202752,
|
||||||
maxTokens: 16384,
|
maxTokens: 131072,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"z-ai/glm-4.6v": {
|
"z-ai/glm-4.6v": {
|
||||||
id: "z-ai/glm-4.6v",
|
id: "z-ai/glm-4.6v",
|
||||||
@@ -4597,13 +4615,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.06,
|
input: 0.0605,
|
||||||
output: 0.4,
|
output: 0.4,
|
||||||
cacheRead: 0.01,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 202752,
|
contextWindow: 131072,
|
||||||
maxTokens: 16384,
|
maxTokens: 131072,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"z-ai/glm-5": {
|
"z-ai/glm-5": {
|
||||||
id: "z-ai/glm-5",
|
id: "z-ai/glm-5",
|
||||||
@@ -4620,8 +4638,8 @@ export const OPENROUTER_MODELS = {
|
|||||||
cacheRead: 0.119,
|
cacheRead: 0.119,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 198000,
|
contextWindow: 202752,
|
||||||
maxTokens: 128000,
|
maxTokens: 202752,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"z-ai/glm-5-turbo": {
|
"z-ai/glm-5-turbo": {
|
||||||
id: "z-ai/glm-5-turbo",
|
id: "z-ai/glm-5-turbo",
|
||||||
@@ -4638,7 +4656,7 @@ export const OPENROUTER_MODELS = {
|
|||||||
cacheRead: 0.24,
|
cacheRead: 0.24,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 262144,
|
contextWindow: 202752,
|
||||||
maxTokens: 131072,
|
maxTokens: 131072,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"z-ai/glm-5.1": {
|
"z-ai/glm-5.1": {
|
||||||
@@ -4670,13 +4688,13 @@ export const OPENROUTER_MODELS = {
|
|||||||
thinkingLevelMap: {"xhigh":"xhigh"},
|
thinkingLevelMap: {"xhigh":"xhigh"},
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.924,
|
input: 0.9366,
|
||||||
output: 2.904,
|
output: 2.9436,
|
||||||
cacheRead: 0.1716,
|
cacheRead: 0.17394,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 1024000,
|
contextWindow: 1048576,
|
||||||
maxTokens: 128000,
|
maxTokens: 131072,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"z-ai/glm-5v-turbo": {
|
"z-ai/glm-5v-turbo": {
|
||||||
id: "z-ai/glm-5v-turbo",
|
id: "z-ai/glm-5v-turbo",
|
||||||
|
|||||||
@@ -622,6 +622,25 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
|||||||
contextWindow: 1000000,
|
contextWindow: 1000000,
|
||||||
maxTokens: 128000,
|
maxTokens: 128000,
|
||||||
} satisfies Model<"anthropic-messages">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
|
"anthropic/claude-opus-4.7-fast": {
|
||||||
|
id: "anthropic/claude-opus-4.7-fast",
|
||||||
|
name: "Claude Opus 4.7 (Fast)",
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "vercel-ai-gateway",
|
||||||
|
baseUrl: "https://ai-gateway.vercel.sh",
|
||||||
|
compat: {"forceAdaptiveThinking":true,"supportsTemperature":false},
|
||||||
|
reasoning: true,
|
||||||
|
thinkingLevelMap: {"xhigh":"xhigh","max":"max"},
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 30,
|
||||||
|
output: 150,
|
||||||
|
cacheRead: 3,
|
||||||
|
cacheWrite: 37.5,
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
maxTokens: 128000,
|
||||||
|
} satisfies Model<"anthropic-messages">,
|
||||||
"anthropic/claude-opus-4.8": {
|
"anthropic/claude-opus-4.8": {
|
||||||
id: "anthropic/claude-opus-4.8",
|
id: "anthropic/claude-opus-4.8",
|
||||||
name: "Claude Opus 4.8",
|
name: "Claude Opus 4.8",
|
||||||
@@ -641,6 +660,25 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
|||||||
contextWindow: 1000000,
|
contextWindow: 1000000,
|
||||||
maxTokens: 128000,
|
maxTokens: 128000,
|
||||||
} satisfies Model<"anthropic-messages">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
|
"anthropic/claude-opus-4.8-fast": {
|
||||||
|
id: "anthropic/claude-opus-4.8-fast",
|
||||||
|
name: "Claude Opus 4.8 (Fast)",
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "vercel-ai-gateway",
|
||||||
|
baseUrl: "https://ai-gateway.vercel.sh",
|
||||||
|
compat: {"forceAdaptiveThinking":true,"supportsTemperature":false},
|
||||||
|
reasoning: true,
|
||||||
|
thinkingLevelMap: {"xhigh":"xhigh","max":"max"},
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 10,
|
||||||
|
output: 50,
|
||||||
|
cacheRead: 1,
|
||||||
|
cacheWrite: 12.5,
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
maxTokens: 128000,
|
||||||
|
} satisfies Model<"anthropic-messages">,
|
||||||
"anthropic/claude-sonnet-4": {
|
"anthropic/claude-sonnet-4": {
|
||||||
id: "anthropic/claude-sonnet-4",
|
id: "anthropic/claude-sonnet-4",
|
||||||
name: "Claude Sonnet 4",
|
name: "Claude Sonnet 4",
|
||||||
@@ -841,8 +879,8 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
|||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.21,
|
input: 0.25,
|
||||||
output: 0.79,
|
output: 0.95,
|
||||||
cacheRead: 0.13,
|
cacheRead: 0.13,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
@@ -2717,6 +2755,23 @@ export const VERCEL_AI_GATEWAY_MODELS = {
|
|||||||
contextWindow: 256000,
|
contextWindow: 256000,
|
||||||
maxTokens: 256000,
|
maxTokens: 256000,
|
||||||
} satisfies Model<"anthropic-messages">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
|
"thinkingmachines/inkling": {
|
||||||
|
id: "thinkingmachines/inkling",
|
||||||
|
name: "Inkling",
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "vercel-ai-gateway",
|
||||||
|
baseUrl: "https://ai-gateway.vercel.sh",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 1,
|
||||||
|
output: 4.05,
|
||||||
|
cacheRead: 0.17,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 256000,
|
||||||
|
maxTokens: 256000,
|
||||||
|
} satisfies Model<"anthropic-messages">,
|
||||||
"xai/grok-4.1-fast-non-reasoning": {
|
"xai/grok-4.1-fast-non-reasoning": {
|
||||||
id: "xai/grok-4.1-fast-non-reasoning",
|
id: "xai/grok-4.1-fast-non-reasoning",
|
||||||
name: "Grok 4.1 Fast Non-Reasoning",
|
name: "Grok 4.1 Fast Non-Reasoning",
|
||||||
|
|||||||
@@ -97,11 +97,12 @@ export const XAI_MODELS = {
|
|||||||
"grok-4.5": {
|
"grok-4.5": {
|
||||||
id: "grok-4.5",
|
id: "grok-4.5",
|
||||||
name: "Grok 4.5",
|
name: "Grok 4.5",
|
||||||
api: "openai-completions",
|
api: "openai-responses",
|
||||||
provider: "xai",
|
provider: "xai",
|
||||||
baseUrl: "https://api.x.ai/v1",
|
baseUrl: "https://api.x.ai/v1",
|
||||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false},
|
compat: {"supportsLongCacheRetention":false},
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
|
thinkingLevelMap: {"off":null,"minimal":null},
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 2,
|
input: 2,
|
||||||
@@ -111,7 +112,7 @@ export const XAI_MODELS = {
|
|||||||
},
|
},
|
||||||
contextWindow: 500000,
|
contextWindow: 500000,
|
||||||
maxTokens: 500000,
|
maxTokens: 500000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-responses">,
|
||||||
"grok-build-0.1": {
|
"grok-build-0.1": {
|
||||||
id: "grok-build-0.1",
|
id: "grok-build-0.1",
|
||||||
name: "Grok Build 0.1",
|
name: "Grok Build 0.1",
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
|
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
|
||||||
import { envApiKeyAuth } from "../auth/helpers.ts";
|
import { openAIResponsesApi } from "../api/openai-responses.lazy.ts";
|
||||||
|
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
|
||||||
|
import { loadXaiOAuth } from "../auth/oauth/load.ts";
|
||||||
import { createProvider, type Provider } from "../models.ts";
|
import { createProvider, type Provider } from "../models.ts";
|
||||||
import { XAI_MODELS } from "./xai.models.ts";
|
import { XAI_MODELS } from "./xai.models.ts";
|
||||||
|
|
||||||
export function xaiProvider(): Provider<"openai-completions"> {
|
export function xaiProvider(): Provider<"openai-completions" | "openai-responses"> {
|
||||||
return createProvider({
|
return createProvider({
|
||||||
id: "xai",
|
id: "xai",
|
||||||
name: "xAI",
|
name: "xAI",
|
||||||
baseUrl: "https://api.x.ai/v1",
|
baseUrl: "https://api.x.ai/v1",
|
||||||
auth: { apiKey: envApiKeyAuth("xAI API key", ["XAI_API_KEY"]) },
|
auth: {
|
||||||
|
apiKey: envApiKeyAuth("xAI API key", ["XAI_API_KEY"]),
|
||||||
|
oauth: lazyOAuth({ name: "xAI (Grok/X subscription)", load: loadXaiOAuth }),
|
||||||
|
},
|
||||||
models: Object.values(XAI_MODELS),
|
models: Object.values(XAI_MODELS),
|
||||||
api: openAICompletionsApi(),
|
api: {
|
||||||
|
"openai-completions": openAICompletionsApi(),
|
||||||
|
"openai-responses": openAIResponsesApi(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,8 +283,9 @@ describe("Models runtime", () => {
|
|||||||
expect(offline.getModel("dynamic", "fetched")).toBeDefined();
|
expect(offline.getModel("dynamic", "fetched")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes effective API-key credentials and skips unconfigured providers", async () => {
|
it("passes effective API-key credentials and refresh options while skipping unconfigured providers", async () => {
|
||||||
let effectiveCredential: unknown;
|
let effectiveCredential: unknown;
|
||||||
|
let forceRefresh: boolean | undefined;
|
||||||
let unconfiguredRefreshes = 0;
|
let unconfiguredRefreshes = 0;
|
||||||
const models = createModels();
|
const models = createModels();
|
||||||
models.setProvider(
|
models.setProvider(
|
||||||
@@ -293,6 +294,7 @@ describe("Models runtime", () => {
|
|||||||
auth: { apiKey: envKeyAuth("ambient-key") },
|
auth: { apiKey: envKeyAuth("ambient-key") },
|
||||||
refreshModels: async (context) => {
|
refreshModels: async (context) => {
|
||||||
effectiveCredential = context.credential;
|
effectiveCredential = context.credential;
|
||||||
|
forceRefresh = context.force;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -306,8 +308,9 @@ describe("Models runtime", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await models.refresh();
|
await models.refresh({ force: true });
|
||||||
expect(effectiveCredential).toEqual({ type: "api_key", key: "ambient-key", env: undefined });
|
expect(effectiveCredential).toEqual({ type: "api_key", key: "ambient-key", env: undefined });
|
||||||
|
expect(forceRefresh).toBe(true);
|
||||||
expect(unconfiguredRefreshes).toBe(0);
|
expect(unconfiguredRefreshes).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { InMemoryCredentialStore } from "../src/auth/credential-store.ts";
|
|||||||
import { anthropicOAuth } from "../src/auth/oauth/anthropic.ts";
|
import { anthropicOAuth } from "../src/auth/oauth/anthropic.ts";
|
||||||
import { githubCopilotOAuth } from "../src/auth/oauth/github-copilot.ts";
|
import { githubCopilotOAuth } from "../src/auth/oauth/github-copilot.ts";
|
||||||
import { openaiCodexOAuth } from "../src/auth/oauth/openai-codex.ts";
|
import { openaiCodexOAuth } from "../src/auth/oauth/openai-codex.ts";
|
||||||
|
import { xaiOAuth } from "../src/auth/oauth/xai.ts";
|
||||||
import { createModels } from "../src/models.ts";
|
import { createModels } from "../src/models.ts";
|
||||||
import * as extensionOAuthCompatibility from "../src/oauth.ts";
|
import * as extensionOAuthCompatibility from "../src/oauth.ts";
|
||||||
import { anthropicProvider } from "../src/providers/anthropic.ts";
|
import { anthropicProvider } from "../src/providers/anthropic.ts";
|
||||||
@@ -32,6 +33,11 @@ describe.sequential("OAuthAuth adapters", () => {
|
|||||||
expect(auth).toEqual({ apiKey: "token" });
|
expect(auth).toEqual({ apiKey: "token" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("xAI toAuth derives the api key from the access token", async () => {
|
||||||
|
const auth = await xaiOAuth.toAuth({ type: "oauth", access: "token", refresh: "r", expires: 0 });
|
||||||
|
expect(auth).toEqual({ apiKey: "token" });
|
||||||
|
});
|
||||||
|
|
||||||
it("github-copilot toAuth derives baseUrl from the token proxy endpoint", async () => {
|
it("github-copilot toAuth derives baseUrl from the token proxy endpoint", async () => {
|
||||||
const access = "tid=abc;exp=123;proxy-ep=proxy.enterprise.example;rest";
|
const access = "tid=abc;exp=123;proxy-ep=proxy.enterprise.example;rest";
|
||||||
const auth = await githubCopilotOAuth.toAuth({ type: "oauth", access, refresh: "r", expires: 0 });
|
const auth = await githubCopilotOAuth.toAuth({ type: "oauth", access, refresh: "r", expires: 0 });
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { xaiOAuth } from "../src/auth/oauth/xai.ts";
|
||||||
|
import type { OAuthCredential } from "../src/auth/types.ts";
|
||||||
|
|
||||||
|
function jsonResponse(body: unknown, status = 200): Response {
|
||||||
|
return new Response(JSON.stringify(body), {
|
||||||
|
status,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestUrl(input: unknown): string {
|
||||||
|
if (typeof input === "string") return input;
|
||||||
|
if (input instanceof URL) return input.toString();
|
||||||
|
if (input instanceof Request) return input.url;
|
||||||
|
throw new Error(`Unsupported request input: ${String(input)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestForm(init: RequestInit | undefined): URLSearchParams {
|
||||||
|
return new URLSearchParams(String(init?.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deviceCodeResponse(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
device_code: "device-code",
|
||||||
|
user_code: "ABCD-1234",
|
||||||
|
verification_uri: "https://accounts.x.ai/oauth2/device",
|
||||||
|
expires_in: 900,
|
||||||
|
interval: 5,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenResponse(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
access_token: "access-token",
|
||||||
|
refresh_token: "refresh-token",
|
||||||
|
expires_in: 21_600,
|
||||||
|
token_type: "Bearer",
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceCodeInfo = {
|
||||||
|
userCode: string;
|
||||||
|
verificationUri: string;
|
||||||
|
intervalSeconds?: number;
|
||||||
|
expiresInSeconds?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function loginXaiForTest(options: {
|
||||||
|
onDeviceCode: (info: DeviceCodeInfo) => void;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}): Promise<OAuthCredential> {
|
||||||
|
return xaiOAuth.login({
|
||||||
|
signal: options.signal,
|
||||||
|
prompt: () => {
|
||||||
|
throw new Error("Unexpected prompt");
|
||||||
|
},
|
||||||
|
notify: (event) => {
|
||||||
|
if (event.type === "device_code") {
|
||||||
|
const { type: _, ...info } = event;
|
||||||
|
options.onDeviceCode(info);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshXaiForTest(refreshToken: string): Promise<OAuthCredential> {
|
||||||
|
return xaiOAuth.refresh({ type: "oauth", access: "old-access", refresh: refreshToken, expires: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("xAI OAuth device flow", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the device grant, delays polling, and handles pending and slow_down", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const startTime = new Date("2026-07-09T20:00:00Z");
|
||||||
|
vi.setSystemTime(startTime);
|
||||||
|
const pollTimes: number[] = [];
|
||||||
|
const tokenReplies = [
|
||||||
|
jsonResponse({ error: "authorization_pending" }, 400),
|
||||||
|
jsonResponse({ error: "slow_down", interval: 10 }, 400),
|
||||||
|
jsonResponse(tokenResponse()),
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetchMock = vi.fn(async (input: unknown, init?: RequestInit) => {
|
||||||
|
const url = requestUrl(input);
|
||||||
|
|
||||||
|
if (url === "https://auth.x.ai/oauth2/device/code") {
|
||||||
|
const form = requestForm(init);
|
||||||
|
expect(form.get("client_id")).toBe("b1a00492-073a-47ea-816f-4c329264a828");
|
||||||
|
expect(form.get("scope")).toBe("openid profile email offline_access grok-cli:access api:access");
|
||||||
|
expect(form.get("referrer")).toBe("pi");
|
||||||
|
return jsonResponse(deviceCodeResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === "https://auth.x.ai/oauth2/token") {
|
||||||
|
pollTimes.push(Date.now());
|
||||||
|
const form = requestForm(init);
|
||||||
|
expect(form.get("grant_type")).toBe("urn:ietf:params:oauth:grant-type:device_code");
|
||||||
|
expect(form.get("client_id")).toBe("b1a00492-073a-47ea-816f-4c329264a828");
|
||||||
|
expect(form.get("device_code")).toBe("device-code");
|
||||||
|
const reply = tokenReplies.shift();
|
||||||
|
if (!reply) throw new Error("Unexpected token poll");
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unexpected request: ${url}`);
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
const deviceCodes: DeviceCodeInfo[] = [];
|
||||||
|
const loginPromise = loginXaiForTest({ onDeviceCode: (info) => deviceCodes.push(info) });
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
expect(deviceCodes).toEqual([
|
||||||
|
{
|
||||||
|
userCode: "ABCD-1234",
|
||||||
|
verificationUri: "https://accounts.x.ai/oauth2/device",
|
||||||
|
intervalSeconds: 5,
|
||||||
|
expiresInSeconds: 900,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(pollTimes).toEqual([]);
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
|
expect(pollTimes).toEqual([startTime.getTime() + 5000]);
|
||||||
|
|
||||||
|
// slow_down raised the interval to 10 seconds
|
||||||
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
|
expect(pollTimes).toEqual([startTime.getTime() + 5000, startTime.getTime() + 10_000]);
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(10_000);
|
||||||
|
const credentials = await loginPromise;
|
||||||
|
expect(pollTimes).toEqual([
|
||||||
|
startTime.getTime() + 5000,
|
||||||
|
startTime.getTime() + 10_000,
|
||||||
|
startTime.getTime() + 20_000,
|
||||||
|
]);
|
||||||
|
expect(credentials).toEqual({
|
||||||
|
type: "oauth",
|
||||||
|
access: "access-token",
|
||||||
|
refresh: "refresh-token",
|
||||||
|
expires: startTime.getTime() + 20_000 + 21_600_000 - 300_000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the default poll interval when the response reports interval 0", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const startTime = new Date("2026-07-09T20:00:00Z");
|
||||||
|
vi.setSystemTime(startTime);
|
||||||
|
const pollTimes: number[] = [];
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(async (input: unknown) => {
|
||||||
|
if (requestUrl(input) === "https://auth.x.ai/oauth2/device/code") {
|
||||||
|
return jsonResponse(deviceCodeResponse({ interval: 0 }));
|
||||||
|
}
|
||||||
|
pollTimes.push(Date.now());
|
||||||
|
return jsonResponse(tokenResponse());
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginPromise = loginXaiForTest({ onDeviceCode: () => {} });
|
||||||
|
// RFC 8628 default interval is 5 seconds when the server does not require a wait.
|
||||||
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
|
await loginPromise;
|
||||||
|
expect(pollTimes).toEqual([startTime.getTime() + 5000]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["http://accounts.x.ai/oauth2/device", "file:///etc/passwd", "not a url"])(
|
||||||
|
"rejects a non-https verification URI: %s",
|
||||||
|
async (verificationUri) => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(async () => jsonResponse(deviceCodeResponse({ verification_uri: verificationUri }))),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(loginXaiForTest({ onDeviceCode: () => {} })).rejects.toThrow("Untrusted verification URI");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(["access_denied", "authorization_denied"])(
|
||||||
|
"fails when device authorization is denied: %s",
|
||||||
|
async (error) => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
let requestCount = 0;
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(async () => {
|
||||||
|
requestCount += 1;
|
||||||
|
return requestCount === 1
|
||||||
|
? jsonResponse(deviceCodeResponse({ interval: 1 }))
|
||||||
|
: jsonResponse({ error }, 400);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginPromise = loginXaiForTest({ onDeviceCode: () => {} });
|
||||||
|
const assertion = expect(loginPromise).rejects.toThrow("xAI device authorization was denied");
|
||||||
|
await vi.advanceTimersByTimeAsync(1000);
|
||||||
|
await assertion;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("cancels while waiting for the first token poll", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const controller = new AbortController();
|
||||||
|
const fetchMock = vi.fn(async () => jsonResponse(deviceCodeResponse()));
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
const loginPromise = loginXaiForTest({
|
||||||
|
onDeviceCode: () => controller.abort(),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(loginPromise).rejects.toThrow("Login cancelled");
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refreshes tokens and preserves an unrotated refresh token", async () => {
|
||||||
|
let requestCount = 0;
|
||||||
|
const fetchMock = vi.fn(async (input: unknown, init?: RequestInit) => {
|
||||||
|
expect(requestUrl(input)).toBe("https://auth.x.ai/oauth2/token");
|
||||||
|
const form = requestForm(init);
|
||||||
|
expect(form.get("grant_type")).toBe("refresh_token");
|
||||||
|
expect(form.get("client_id")).toBe("b1a00492-073a-47ea-816f-4c329264a828");
|
||||||
|
requestCount += 1;
|
||||||
|
if (requestCount === 1) {
|
||||||
|
expect(form.get("refresh_token")).toBe("old-refresh");
|
||||||
|
return jsonResponse(tokenResponse({ access_token: "new-access", refresh_token: "new-refresh" }));
|
||||||
|
}
|
||||||
|
expect(form.get("refresh_token")).toBe("keep-refresh");
|
||||||
|
return jsonResponse(tokenResponse({ access_token: "newer-access", refresh_token: undefined }));
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
const rotated = await refreshXaiForTest("old-refresh");
|
||||||
|
const preserved = await refreshXaiForTest("keep-refresh");
|
||||||
|
expect(rotated.type).toBe("oauth");
|
||||||
|
expect(rotated.refresh).toBe("new-refresh");
|
||||||
|
expect(rotated.access).toBe("new-access");
|
||||||
|
expect(preserved.refresh).toBe("keep-refresh");
|
||||||
|
expect(preserved.access).toBe("newer-access");
|
||||||
|
expect(xaiOAuth.name).toBe("xAI (Grok/X subscription)");
|
||||||
|
await expect(xaiOAuth.toAuth(preserved)).resolves.toEqual({ apiKey: "newer-access" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("assumes a one-hour lifetime when expires_in is missing", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const startTime = new Date("2026-07-09T20:00:00Z");
|
||||||
|
vi.setSystemTime(startTime);
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(async () => jsonResponse(tokenResponse({ expires_in: undefined }))),
|
||||||
|
);
|
||||||
|
|
||||||
|
const credentials = await refreshXaiForTest("old-refresh");
|
||||||
|
expect(credentials.expires).toBe(startTime.getTime() + 3_600_000 - 300_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects token responses with missing fields", async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(async () => jsonResponse(tokenResponse({ access_token: undefined }))),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(refreshXaiForTest("old-refresh")).rejects.toThrow("Invalid xAI OAuth response field: access_token");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("surfaces the upstream error code and description on refresh failure", async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(async () => jsonResponse({ error: "invalid_grant", error_description: "refresh token revoked" }, 400)),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(refreshXaiForTest("old-refresh")).rejects.toThrow(
|
||||||
|
"xAI OAuth token refresh failed (HTTP 400): invalid_grant: refresh token revoked",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { OpenAIResponsesOptions } from "../src/api/openai-responses.ts";
|
||||||
|
import { getSupportedThinkingLevels } from "../src/models.ts";
|
||||||
|
import { XAI_MODELS } from "../src/providers/xai.models.ts";
|
||||||
|
import { xaiProvider } from "../src/providers/xai.ts";
|
||||||
|
import type { Context, Model } from "../src/types.ts";
|
||||||
|
|
||||||
|
type CapturedRequest = {
|
||||||
|
url: string;
|
||||||
|
headers: Headers;
|
||||||
|
body: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function completedResponse(): Response {
|
||||||
|
const event = {
|
||||||
|
type: "response.completed",
|
||||||
|
sequence_number: 0,
|
||||||
|
response: {
|
||||||
|
id: "resp_xai_test",
|
||||||
|
status: "completed",
|
||||||
|
output: [],
|
||||||
|
usage: {
|
||||||
|
input_tokens: 1,
|
||||||
|
output_tokens: 1,
|
||||||
|
total_tokens: 2,
|
||||||
|
input_tokens_details: { cached_tokens: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return new Response(`data: ${JSON.stringify(event)}\n\ndata: [DONE]\n\n`, {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "text/event-stream" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureRequest(
|
||||||
|
model: Model<"openai-responses">,
|
||||||
|
context: Context,
|
||||||
|
options: OpenAIResponsesOptions,
|
||||||
|
): Promise<CapturedRequest> {
|
||||||
|
let captured: CapturedRequest | undefined;
|
||||||
|
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
|
||||||
|
const request = new Request(input, init);
|
||||||
|
captured = {
|
||||||
|
url: request.url,
|
||||||
|
headers: request.headers,
|
||||||
|
body: JSON.parse(await request.clone().text()) as Record<string, unknown>,
|
||||||
|
};
|
||||||
|
return completedResponse();
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await xaiProvider().stream(model, context, options).result();
|
||||||
|
expect(result.stopReason, result.errorMessage).toBe("stop");
|
||||||
|
expect(captured).toBeDefined();
|
||||||
|
return captured!;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("xAI Responses provider", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses Responses with low/medium/high efforts only for Grok 4.5", () => {
|
||||||
|
expect(XAI_MODELS["grok-4.5"].api).toBe("openai-responses");
|
||||||
|
expect(getSupportedThinkingLevels(XAI_MODELS["grok-4.5"])).toEqual(["low", "medium", "high"]);
|
||||||
|
expect(XAI_MODELS["grok-4.3"].api).toBe("openai-completions");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses /responses with bearer auth and xAI-compatible request fields", async () => {
|
||||||
|
const captured = await captureRequest(
|
||||||
|
XAI_MODELS["grok-4.5"],
|
||||||
|
{
|
||||||
|
systemPrompt: "You are a careful coding assistant.",
|
||||||
|
messages: [{ role: "user", content: "hello", timestamp: 1 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiKey: "xai-test-token",
|
||||||
|
sessionId: "pi-session-123",
|
||||||
|
cacheRetention: "long",
|
||||||
|
reasoningEffort: "medium",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(captured.url).toBe("https://api.x.ai/v1/responses");
|
||||||
|
expect(captured.headers.get("authorization")).toBe("Bearer xai-test-token");
|
||||||
|
expect(captured.headers.get("session_id")).toBe("pi-session-123");
|
||||||
|
expect(captured.body).toMatchObject({
|
||||||
|
model: "grok-4.5",
|
||||||
|
store: false,
|
||||||
|
stream: true,
|
||||||
|
prompt_cache_key: "pi-session-123",
|
||||||
|
reasoning: { effort: "medium" },
|
||||||
|
include: ["reasoning.encrypted_content"],
|
||||||
|
});
|
||||||
|
expect(captured.body).not.toHaveProperty("prompt_cache_retention");
|
||||||
|
expect(captured.body.input).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
role: "developer",
|
||||||
|
content: "You are a careful coding assistant.",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.80.8] - 2026-07-16
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- **Unified model runtime and provider authentication** — `ModelRuntime` centralizes model configuration, provider-owned `/login`, and dynamic provider catalogs. See [Providers](docs/providers.md).
|
||||||
|
- **Live model catalog refresh** — `/model` refreshes configured providers in the background, and `pi update --models` forces an immediate refresh. See [Install and Manage](docs/packages.md#install-and-manage).
|
||||||
|
- **xAI device-code OAuth and Grok 4.5 Responses support** — Sign in to xAI with a device code and use Grok 4.5 with low, medium, or high thinking. See [xAI](docs/providers.md#xai-grokx-subscription).
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
- Replaced the SDK's `CreateAgentSessionOptions.authStorage` and `modelRegistry` options with the async `modelRuntime` option. `AuthStorage` and its storage backends are no longer exported; use `ModelRuntime` (or a custom pi-ai `CredentialStore`), or `readStoredCredential()` for one-off reads of auth.json.
|
- Replaced the SDK's `CreateAgentSessionOptions.authStorage` and `modelRegistry` options with the async `modelRuntime` option. `AuthStorage` and its storage backends are no longer exported; use `ModelRuntime` (or a custom pi-ai `CredentialStore`), or `readStoredCredential()` for one-off reads of auth.json.
|
||||||
@@ -16,6 +24,8 @@
|
|||||||
- Added provider-owned `/login` discovery directly from registered pi-ai providers, including ambient auth status and informational links.
|
- Added provider-owned `/login` discovery directly from registered pi-ai providers, including ambient auth status and informational links.
|
||||||
- Added file-backed dynamic catalogs in `models-store.json`, per-provider pi.dev catalog overlays, and Radius gateway support including offline migration from legacy credential-cached catalogs.
|
- Added file-backed dynamic catalogs in `models-store.json`, per-provider pi.dev catalog overlays, and Radius gateway support including offline migration from legacy credential-cached catalogs.
|
||||||
- Added extension provider `refreshModels(context)` support for dynamic model discovery with optional provider-controlled persistence.
|
- Added extension provider `refreshModels(context)` support for dynamic model discovery with optional provider-controlled persistence.
|
||||||
|
- Added `pi update --models` to force an immediate model catalog refresh without updating pi or extensions.
|
||||||
|
- Added inherited xAI device-code OAuth login and Grok 4.5 OpenAI Responses support, with low, medium, and high thinking levels ([#6651](https://github.com/earendil-works/pi-mono/pull/6651) by [@Jaaneek](https://github.com/Jaaneek)).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -27,6 +37,10 @@
|
|||||||
|
|
||||||
- Fixed configured-provider catalog refresh to parse pi.dev's model-ID keyed responses, throttle checks to once per four hours, send the versioned pi user agent, treat unimplemented routes as unavailable overlays, and show concise refresh status in `/model`.
|
- Fixed configured-provider catalog refresh to parse pi.dev's model-ID keyed responses, throttle checks to once per four hours, send the versioned pi user agent, treat unimplemented routes as unavailable overlays, and show concise refresh status in `/model`.
|
||||||
- Fixed adjacent assistant thinking blocks to render as one thinking section.
|
- Fixed adjacent assistant thinking blocks to render as one thinking section.
|
||||||
|
- Fixed inherited OpenAI Codex session IDs longer than 64 characters to meet the API limit ([#6630](https://github.com/earendil-works/pi-mono/issues/6630)).
|
||||||
|
- Fixed inherited terminal output to normalize tab characters consistently ([#6697](https://github.com/earendil-works/pi-mono/pull/6697) by [@xz-dev](https://github.com/xz-dev)).
|
||||||
|
- Fixed the Windows terminal title after checking npm packages ([#6629](https://github.com/earendil-works/pi-mono/issues/6629)).
|
||||||
|
- Fixed Bun standalone binaries to bundle OAuth adapters for interactive logins.
|
||||||
|
|
||||||
## [0.80.7] - 2026-07-14
|
## [0.80.7] - 2026-07-14
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ Then just talk to pi. By default, pi gives the model four tools: `read`, `write`
|
|||||||
|
|
||||||
## Providers & Models
|
## Providers & Models
|
||||||
|
|
||||||
For each built-in provider, pi maintains a list of tool-capable models, updated with every release. Authenticate via subscription (`/login`) or API key, then select any model from that provider via `/model` (or Ctrl+L).
|
For each built-in provider, pi maintains a list of tool-capable models. Configured provider catalogs refresh automatically; run `pi update --models` to force an immediate refresh. Authenticate via subscription (`/login`) or API key, then select any model from that provider via `/model` (or Ctrl+L).
|
||||||
|
|
||||||
**Subscriptions:**
|
**Subscriptions:**
|
||||||
- Anthropic Claude Pro/Max
|
- Anthropic Claude Pro/Max
|
||||||
@@ -421,6 +421,7 @@ pi list
|
|||||||
pi update # update pi only
|
pi update # update pi only
|
||||||
pi update --all # update pi and packages
|
pi update --all # update pi and packages
|
||||||
pi update --extensions # update packages only
|
pi update --extensions # update packages only
|
||||||
|
pi update --models # refresh model catalogs only
|
||||||
pi update --self # update pi only
|
pi update --self # update pi only
|
||||||
pi update --self --force # reinstall pi even if current
|
pi update --self --force # reinstall pi even if current
|
||||||
pi update npm:@foo/pi-tools # update one package
|
pi update npm:@foo/pi-tools # update one package
|
||||||
@@ -519,6 +520,7 @@ pi uninstall <source> [-l] # Alias for remove
|
|||||||
pi update [source|self|pi] # Update pi only, or one package source
|
pi update [source|self|pi] # Update pi only, or one package source
|
||||||
pi update --all # Update pi and packages
|
pi update --all # Update pi and packages
|
||||||
pi update --extensions # Update packages only
|
pi update --extensions # Update packages only
|
||||||
|
pi update --models # Refresh model catalogs only
|
||||||
pi update --self # Update pi only
|
pi update --self # Update pi only
|
||||||
pi update --self --force # Reinstall pi even if current
|
pi update --self --force # Reinstall pi even if current
|
||||||
pi update --extension <src> # Update one package
|
pi update --extension <src> # Update one package
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ pi list # show installed packages from settings
|
|||||||
pi update # update pi only
|
pi update # update pi only
|
||||||
pi update --all # update pi, update packages, and reconcile pinned git refs
|
pi update --all # update pi, update packages, and reconcile pinned git refs
|
||||||
pi update --extensions # update packages and reconcile pinned git refs only
|
pi update --extensions # update packages and reconcile pinned git refs only
|
||||||
|
pi update --models # refresh model catalogs only
|
||||||
pi update --self # update pi only
|
pi update --self # update pi only
|
||||||
pi update --self --force # reinstall pi even if current
|
pi update --self --force # reinstall pi even if current
|
||||||
pi update npm:@foo/bar # update one package
|
pi update npm:@foo/bar # update one package
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ Use `/login` in interactive mode, then select a provider:
|
|||||||
- ChatGPT Plus/Pro (Codex)
|
- ChatGPT Plus/Pro (Codex)
|
||||||
- Claude Pro/Max
|
- Claude Pro/Max
|
||||||
- GitHub Copilot
|
- GitHub Copilot
|
||||||
|
- xAI (Grok/X subscription)
|
||||||
- Radius
|
- Radius
|
||||||
|
|
||||||
Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json` and auto-refresh when expired.
|
Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json` and auto-refresh when expired.
|
||||||
@@ -36,6 +37,11 @@ Anthropic subscription auth is active for Claude Pro/Max accounts. Third-party h
|
|||||||
- Press Enter for github.com, or enter your GitHub Enterprise Server domain
|
- Press Enter for github.com, or enter your GitHub Enterprise Server domain
|
||||||
- If you get "model not supported", enable it in VS Code: Copilot Chat → model selector → select model → "Enable"
|
- If you get "model not supported", enable it in VS Code: Copilot Chat → model selector → select model → "Enable"
|
||||||
|
|
||||||
|
### xAI (Grok/X subscription)
|
||||||
|
|
||||||
|
- Run `/login xai`, then select **Use a subscription**
|
||||||
|
- `XAI_API_KEY` remains available through **Use an API key**
|
||||||
|
|
||||||
### Radius
|
### Radius
|
||||||
|
|
||||||
Radius is a dynamic `pi-messages` gateway. `/login radius` stores OAuth tokens in `auth.json`; the gateway catalog is refreshed independently and cached in `models-store.json`. Custom Radius gateways can be declared in `models.json` with `"oauth": "radius"` and a gateway `baseUrl`.
|
Radius is a dynamic `pi-messages` gateway. `/login radius` stores OAuth tokens in `auth.json`; the gateway catalog is refreshed independently and cached in `models-store.json`. Custom Radius gateways can be declared in `models.json` with `"oauth": "radius"` and a gateway `baseUrl`.
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ pi uninstall <source> [-l] # Alias for remove
|
|||||||
pi update [source|self|pi] # Update pi only, or one package source
|
pi update [source|self|pi] # Update pi only, or one package source
|
||||||
pi update --all # Update pi and packages; reconcile pinned git refs
|
pi update --all # Update pi and packages; reconcile pinned git refs
|
||||||
pi update --extensions # Update packages only; reconcile pinned git refs
|
pi update --extensions # Update packages only; reconcile pinned git refs
|
||||||
|
pi update --models # Refresh model catalogs only
|
||||||
pi update --self # Update pi only
|
pi update --self # Update pi only
|
||||||
pi update --extension <src> # Update one package
|
pi update --extension <src> # Update one package
|
||||||
pi list # List installed packages
|
pi list # List installed packages
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-extension-custom-provider",
|
"name": "pi-extension-custom-provider",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pi-extension-custom-provider",
|
"name": "pi-extension-custom-provider",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.52.0"
|
"@anthropic-ai/sdk": "^0.52.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-extension-custom-provider-anthropic",
|
"name": "pi-extension-custom-provider-anthropic",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "echo 'nothing to clean'",
|
"clean": "echo 'nothing to clean'",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-extension-custom-provider-gitlab-duo",
|
"name": "pi-extension-custom-provider-gitlab-duo",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "echo 'nothing to clean'",
|
"clean": "echo 'nothing to clean'",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-extension-gondolin",
|
"name": "pi-extension-gondolin",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pi-extension-gondolin",
|
"name": "pi-extension-gondolin",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/gondolin": "0.12.0"
|
"@earendil-works/gondolin": "0.12.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-extension-gondolin",
|
"name": "pi-extension-gondolin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "echo 'nothing to clean'",
|
"clean": "echo 'nothing to clean'",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-extension-sandbox",
|
"name": "pi-extension-sandbox",
|
||||||
"version": "1.10.7",
|
"version": "1.10.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pi-extension-sandbox",
|
"name": "pi-extension-sandbox",
|
||||||
"version": "1.10.7",
|
"version": "1.10.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-extension-sandbox",
|
"name": "pi-extension-sandbox",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.10.7",
|
"version": "1.10.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "echo 'nothing to clean'",
|
"clean": "echo 'nothing to clean'",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-extension-with-deps",
|
"name": "pi-extension-with-deps",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pi-extension-with-deps",
|
"name": "pi-extension-with-deps",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-extension-with-deps",
|
"name": "pi-extension-with-deps",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "echo 'nothing to clean'",
|
"clean": "echo 'nothing to clean'",
|
||||||
|
|||||||
+15
-15
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@earendil-works/pi-coding-agent-install",
|
"name": "@earendil-works/pi-coding-agent-install",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@earendil-works/pi-coding-agent-install",
|
"name": "@earendil-works/pi-coding-agent-install",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "0.80.7"
|
"@earendil-works/pi-coding-agent": "0.80.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.19.0"
|
"node": ">=22.19.0"
|
||||||
@@ -450,11 +450,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-agent-core": {
|
"node_modules/@earendil-works/pi-agent-core": {
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.7.tgz",
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.8.tgz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-ai": "^0.80.7",
|
"@earendil-works/pi-ai": "^0.80.8",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"typebox": "1.1.38",
|
"typebox": "1.1.38",
|
||||||
"yaml": "2.9.0"
|
"yaml": "2.9.0"
|
||||||
@@ -464,8 +464,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-ai": {
|
"node_modules/@earendil-works/pi-ai": {
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.7.tgz",
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.8.tgz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "0.91.1",
|
"@anthropic-ai/sdk": "0.91.1",
|
||||||
@@ -488,13 +488,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-coding-agent": {
|
"node_modules/@earendil-works/pi-coding-agent": {
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.80.7.tgz",
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.80.8.tgz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-agent-core": "^0.80.7",
|
"@earendil-works/pi-agent-core": "^0.80.8",
|
||||||
"@earendil-works/pi-ai": "^0.80.7",
|
"@earendil-works/pi-ai": "^0.80.8",
|
||||||
"@earendil-works/pi-tui": "^0.80.7",
|
"@earendil-works/pi-tui": "^0.80.8",
|
||||||
"@silvia-odwyer/photon-node": "0.3.4",
|
"@silvia-odwyer/photon-node": "0.3.4",
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
@@ -522,8 +522,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-tui": {
|
"node_modules/@earendil-works/pi-tui": {
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.7.tgz",
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.8.tgz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-east-asian-width": "1.6.0",
|
"get-east-asian-width": "1.6.0",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "@earendil-works/pi-coding-agent-install",
|
"name": "@earendil-works/pi-coding-agent-install",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Lockfile root used by the Pi installer and updater.",
|
"description": "Lockfile root used by the Pi installer and updater.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "0.80.7"
|
"@earendil-works/pi-coding-agent": "0.80.8"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"rimraf": "6.1.2",
|
"rimraf": "6.1.2",
|
||||||
|
|||||||
+12
-12
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "@earendil-works/pi-coding-agent",
|
"name": "@earendil-works/pi-coding-agent",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@earendil-works/pi-coding-agent",
|
"name": "@earendil-works/pi-coding-agent",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-agent-core": "^0.80.7",
|
"@earendil-works/pi-agent-core": "^0.80.8",
|
||||||
"@earendil-works/pi-ai": "^0.80.7",
|
"@earendil-works/pi-ai": "^0.80.8",
|
||||||
"@earendil-works/pi-tui": "^0.80.7",
|
"@earendil-works/pi-tui": "^0.80.8",
|
||||||
"@silvia-odwyer/photon-node": "0.3.4",
|
"@silvia-odwyer/photon-node": "0.3.4",
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
@@ -474,11 +474,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-agent-core": {
|
"node_modules/@earendil-works/pi-agent-core": {
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.7.tgz",
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.8.tgz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-ai": "^0.80.7",
|
"@earendil-works/pi-ai": "^0.80.8",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"typebox": "1.1.38",
|
"typebox": "1.1.38",
|
||||||
"yaml": "2.9.0"
|
"yaml": "2.9.0"
|
||||||
@@ -488,8 +488,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-ai": {
|
"node_modules/@earendil-works/pi-ai": {
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.7.tgz",
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.8.tgz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "0.91.1",
|
"@anthropic-ai/sdk": "0.91.1",
|
||||||
@@ -512,8 +512,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-tui": {
|
"node_modules/@earendil-works/pi-tui": {
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.7.tgz",
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.8.tgz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-east-asian-width": "1.6.0",
|
"get-east-asian-width": "1.6.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@earendil-works/pi-coding-agent",
|
"name": "@earendil-works/pi-coding-agent",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"piConfig": {
|
"piConfig": {
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
"prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap"
|
"prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-agent-core": "^0.80.7",
|
"@earendil-works/pi-agent-core": "^0.80.8",
|
||||||
"@earendil-works/pi-ai": "^0.80.7",
|
"@earendil-works/pi-ai": "^0.80.8",
|
||||||
"@earendil-works/pi-tui": "^0.80.7",
|
"@earendil-works/pi-tui": "^0.80.8",
|
||||||
"@silvia-odwyer/photon-node": "0.3.4",
|
"@silvia-odwyer/photon-node": "0.3.4",
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
import { registerBunOAuthFlows } from "@earendil-works/pi-ai/bun-oauth";
|
||||||
import { APP_NAME } from "../config.ts";
|
import { APP_NAME } from "../config.ts";
|
||||||
|
|
||||||
process.title = APP_NAME;
|
process.title = APP_NAME;
|
||||||
process.emitWarning = (() => {}) as typeof process.emitWarning;
|
process.emitWarning = (() => {}) as typeof process.emitWarning;
|
||||||
|
|
||||||
|
registerBunOAuthFlows();
|
||||||
|
|
||||||
import { restoreSandboxEnv } from "./restore-sandbox-env.ts";
|
import { restoreSandboxEnv } from "./restore-sandbox-env.ts";
|
||||||
|
|
||||||
restoreSandboxEnv();
|
restoreSandboxEnv();
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ ${chalk.bold("Commands:")}
|
|||||||
${APP_NAME} install <source> [-l] Install extension source and add to settings
|
${APP_NAME} install <source> [-l] Install extension source and add to settings
|
||||||
${APP_NAME} remove <source> [-l] Remove extension source from settings
|
${APP_NAME} remove <source> [-l] Remove extension source from settings
|
||||||
${APP_NAME} uninstall <source> [-l] Alias for remove
|
${APP_NAME} uninstall <source> [-l] Alias for remove
|
||||||
${APP_NAME} update [source|self|pi] Update pi (use --all for pi and extensions)
|
${APP_NAME} update [source|self|pi] Update pi, extensions, or model catalogs
|
||||||
${APP_NAME} list List installed extensions from settings
|
${APP_NAME} list List installed extensions from settings
|
||||||
${APP_NAME} config [-l] Open TUI to enable/disable package resources (Tab switches scope)
|
${APP_NAME} config [-l] Open TUI to enable/disable package resources (Tab switches scope)
|
||||||
${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list/config
|
${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list/config
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export function withRemoteCatalog(provider: Provider, catalogBaseUrl: string = D
|
|||||||
if (stored) dynamicModels = stored.models.filter((model) => model.provider === provider.id);
|
if (stored) dynamicModels = stored.models.filter((model) => model.provider === provider.id);
|
||||||
if (!context.allowNetwork || context.signal?.aborted) return;
|
if (!context.allowNetwork || context.signal?.aborted) return;
|
||||||
if (
|
if (
|
||||||
|
!context.force &&
|
||||||
stored?.checkedAt !== undefined &&
|
stored?.checkedAt !== undefined &&
|
||||||
Date.now() - stored.checkedAt < REMOTE_CATALOG_REFRESH_INTERVAL_MS
|
Date.now() - stored.checkedAt < REMOTE_CATALOG_REFRESH_INTERVAL_MS
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";
|
import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { selectConfig } from "./cli/config-selector.ts";
|
import { selectConfig } from "./cli/config-selector.ts";
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
VERSION,
|
VERSION,
|
||||||
} from "./config.ts";
|
} from "./config.ts";
|
||||||
import type { InlineExtension } from "./core/extensions/types.ts";
|
import type { InlineExtension } from "./core/extensions/types.ts";
|
||||||
|
import { ModelRuntime } from "./core/model-runtime.ts";
|
||||||
import { DefaultPackageManager } from "./core/package-manager.ts";
|
import { DefaultPackageManager } from "./core/package-manager.ts";
|
||||||
import { type AppMode, resolveProjectTrusted } from "./core/project-trust.ts";
|
import { type AppMode, resolveProjectTrusted } from "./core/project-trust.ts";
|
||||||
import { DefaultResourceLoader } from "./core/resource-loader.ts";
|
import { DefaultResourceLoader } from "./core/resource-loader.ts";
|
||||||
@@ -30,7 +32,7 @@ import {
|
|||||||
|
|
||||||
export type PackageCommand = "install" | "remove" | "update" | "list";
|
export type PackageCommand = "install" | "remove" | "update" | "list";
|
||||||
|
|
||||||
type UpdateTarget = { type: "all" } | { type: "self" } | { type: "extensions"; source?: string };
|
type UpdateTarget = { type: "all" } | { type: "self" } | { type: "extensions"; source?: string } | { type: "models" };
|
||||||
|
|
||||||
const SELF_UPDATE_NOTE_MARKDOWN_THEME: MarkdownTheme = {
|
const SELF_UPDATE_NOTE_MARKDOWN_THEME: MarkdownTheme = {
|
||||||
heading: (text) => chalk.bold(chalk.yellow(text)),
|
heading: (text) => chalk.bold(chalk.yellow(text)),
|
||||||
@@ -81,7 +83,7 @@ function getPackageCommandUsage(command: PackageCommand): string {
|
|||||||
case "remove":
|
case "remove":
|
||||||
return `${APP_NAME} remove <source> [-l] [--approve|--no-approve]`;
|
return `${APP_NAME} remove <source> [-l] [--approve|--no-approve]`;
|
||||||
case "update":
|
case "update":
|
||||||
return `${APP_NAME} update [source|self|pi] [--self|--extensions|--all] [--extension <source>] [--approve|--no-approve] [--force]`;
|
return `${APP_NAME} update [source|self|pi] [--self|--extensions|--models|--all] [--extension <source>] [--approve|--no-approve] [--force]`;
|
||||||
case "list":
|
case "list":
|
||||||
return `${APP_NAME} list [--approve|--no-approve]`;
|
return `${APP_NAME} list [--approve|--no-approve]`;
|
||||||
}
|
}
|
||||||
@@ -149,11 +151,12 @@ Examples:
|
|||||||
console.log(`${chalk.bold("Usage:")}
|
console.log(`${chalk.bold("Usage:")}
|
||||||
${getPackageCommandUsage("update")}
|
${getPackageCommandUsage("update")}
|
||||||
|
|
||||||
Update pi and installed packages.
|
Update pi, installed packages, or model catalogs.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--self Update pi only (default when no target is given)
|
--self Update pi only (default when no target is given)
|
||||||
--extensions Update installed packages only
|
--extensions Update installed packages only
|
||||||
|
--models Refresh model catalogs only
|
||||||
--all Update pi and installed packages
|
--all Update pi and installed packages
|
||||||
--extension <source> Update one package only
|
--extension <source> Update one package only
|
||||||
-a, --approve Trust project-local files for this command
|
-a, --approve Trust project-local files for this command
|
||||||
@@ -163,6 +166,7 @@ Options:
|
|||||||
Short forms:
|
Short forms:
|
||||||
${APP_NAME} update Update pi only
|
${APP_NAME} update Update pi only
|
||||||
${APP_NAME} update --all Update pi and all extensions
|
${APP_NAME} update --all Update pi and all extensions
|
||||||
|
${APP_NAME} update --models Refresh model catalogs only
|
||||||
${APP_NAME} update <source> Update one package
|
${APP_NAME} update <source> Update one package
|
||||||
${APP_NAME} update pi Update pi only (self works as alias to pi)
|
${APP_NAME} update pi Update pi only (self works as alias to pi)
|
||||||
`);
|
`);
|
||||||
@@ -205,6 +209,7 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
|
|||||||
let source: string | undefined;
|
let source: string | undefined;
|
||||||
let selfFlag = false;
|
let selfFlag = false;
|
||||||
let extensionsFlag = false;
|
let extensionsFlag = false;
|
||||||
|
let modelsFlag = false;
|
||||||
let allFlag = false;
|
let allFlag = false;
|
||||||
let extensionFlagSource: string | undefined;
|
let extensionFlagSource: string | undefined;
|
||||||
|
|
||||||
@@ -242,6 +247,15 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg === "--models") {
|
||||||
|
if (command === "update") {
|
||||||
|
modelsFlag = true;
|
||||||
|
} else {
|
||||||
|
invalidOption = invalidOption ?? arg;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (arg === "--all") {
|
if (arg === "--all") {
|
||||||
if (command === "update") {
|
if (command === "update") {
|
||||||
allFlag = true;
|
allFlag = true;
|
||||||
@@ -304,15 +318,24 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
|
|||||||
let updateTarget: UpdateTarget | undefined;
|
let updateTarget: UpdateTarget | undefined;
|
||||||
let showExtensionsSkippedNote = false;
|
let showExtensionsSkippedNote = false;
|
||||||
if (command === "update") {
|
if (command === "update") {
|
||||||
if (allFlag && (selfFlag || extensionsFlag || extensionFlagSource)) {
|
if (allFlag && (selfFlag || extensionsFlag || modelsFlag || extensionFlagSource)) {
|
||||||
conflictingOptions =
|
conflictingOptions =
|
||||||
conflictingOptions ?? "--all cannot be combined with --self, --extensions, or --extension";
|
conflictingOptions ?? "--all cannot be combined with --self, --extensions, --models, or --extension";
|
||||||
}
|
}
|
||||||
if (allFlag && source) {
|
if (allFlag && source) {
|
||||||
conflictingOptions = conflictingOptions ?? "--all cannot be combined with a positional source";
|
conflictingOptions = conflictingOptions ?? "--all cannot be combined with a positional source";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extensionFlagSource) {
|
if (modelsFlag) {
|
||||||
|
if (selfFlag || extensionsFlag || allFlag || extensionFlagSource) {
|
||||||
|
conflictingOptions =
|
||||||
|
conflictingOptions ?? "--models cannot be combined with --self, --extensions, --all, or --extension";
|
||||||
|
}
|
||||||
|
if (source) {
|
||||||
|
conflictingOptions = conflictingOptions ?? "--models cannot be combined with a positional source";
|
||||||
|
}
|
||||||
|
updateTarget = { type: "models" };
|
||||||
|
} else if (extensionFlagSource) {
|
||||||
if (selfFlag || extensionsFlag || allFlag) {
|
if (selfFlag || extensionsFlag || allFlag) {
|
||||||
conflictingOptions =
|
conflictingOptions =
|
||||||
conflictingOptions ?? "--extension cannot be combined with --self, --extensions, or --all";
|
conflictingOptions ?? "--extension cannot be combined with --self, --extensions, or --all";
|
||||||
@@ -371,6 +394,33 @@ function updateTargetIncludesExtensions(target: UpdateTarget): boolean {
|
|||||||
return target.type === "all" || target.type === "extensions";
|
return target.type === "all" || target.type === "extensions";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshModelCatalogs(agentDir: string): Promise<void> {
|
||||||
|
const modelRuntime = await ModelRuntime.create({
|
||||||
|
authPath: join(agentDir, "auth.json"),
|
||||||
|
modelsPath: join(agentDir, "models.json"),
|
||||||
|
allowModelNetwork: false,
|
||||||
|
});
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 15_000);
|
||||||
|
try {
|
||||||
|
const result = await modelRuntime.refresh({
|
||||||
|
allowNetwork: true,
|
||||||
|
force: true,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
if (result.aborted) {
|
||||||
|
throw new Error("Model catalog refresh timed out.");
|
||||||
|
}
|
||||||
|
if (result.errors.size > 0) {
|
||||||
|
const details = Array.from(result.errors, ([provider, error]) => `${provider}: ${error.message}`).join("; ");
|
||||||
|
throw new Error(`Could not refresh model catalogs: ${details}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
console.log(chalk.green("Model catalogs refreshed"));
|
||||||
|
}
|
||||||
|
|
||||||
function printSelfUpdateUnavailable(
|
function printSelfUpdateUnavailable(
|
||||||
npmCommand?: string[],
|
npmCommand?: string[],
|
||||||
updatePackageTarget: SelfUpdatePackageTarget = PACKAGE_NAME,
|
updatePackageTarget: SelfUpdatePackageTarget = PACKAGE_NAME,
|
||||||
@@ -673,6 +723,17 @@ export async function handlePackageCommand(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.command === "update" && options.updateTarget?.type === "models") {
|
||||||
|
try {
|
||||||
|
await refreshModelCatalogs(getAgentDir());
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown model catalog refresh error";
|
||||||
|
console.error(chalk.red(`Error: ${message}`));
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const agentDir = getAgentDir();
|
const agentDir = getAgentDir();
|
||||||
const writesProjectPackageConfig = (options.command === "install" || options.command === "remove") && options.local;
|
const writesProjectPackageConfig = (options.command === "install" || options.command === "remove") && options.local;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
|||||||
import { delimiter, join } from "node:path";
|
import { delimiter, join } from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";
|
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";
|
||||||
|
import { ModelRuntime } from "../src/core/model-runtime.ts";
|
||||||
import type { ResolvedPaths } from "../src/core/package-manager.ts";
|
import type { ResolvedPaths } from "../src/core/package-manager.ts";
|
||||||
import { InMemorySettingsStorage, SettingsManager } from "../src/core/settings-manager.ts";
|
import { InMemorySettingsStorage, SettingsManager } from "../src/core/settings-manager.ts";
|
||||||
import { ProjectTrustStore } from "../src/core/trust-manager.ts";
|
import { ProjectTrustStore } from "../src/core/trust-manager.ts";
|
||||||
@@ -371,6 +372,42 @@ describe("package commands", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("refreshes only model catalogs with update --models", async () => {
|
||||||
|
const refresh = vi.fn(async () => ({ aborted: false, errors: new Map<string, Error>() }));
|
||||||
|
const create = vi.spyOn(ModelRuntime, "create").mockResolvedValue({ refresh } as unknown as ModelRuntime);
|
||||||
|
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||||
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
|
await expect(runPackageCommandDirectly(["update", "--models"])).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(create).toHaveBeenCalledWith({
|
||||||
|
authPath: join(agentDir, "auth.json"),
|
||||||
|
modelsPath: join(agentDir, "models.json"),
|
||||||
|
allowModelNetwork: false,
|
||||||
|
});
|
||||||
|
expect(refresh).toHaveBeenCalledWith({
|
||||||
|
allowNetwork: true,
|
||||||
|
force: true,
|
||||||
|
signal: expect.any(AbortSignal),
|
||||||
|
});
|
||||||
|
expect(logSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain("Model catalogs refreshed");
|
||||||
|
expect(errorSpy).not.toHaveBeenCalled();
|
||||||
|
expect(process.exitCode).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects update --models combined with another update target", async () => {
|
||||||
|
const create = vi.spyOn(ModelRuntime, "create");
|
||||||
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
|
await expect(runPackageCommandDirectly(["update", "--models", "--self"])).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(create).not.toHaveBeenCalled();
|
||||||
|
expect(errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
|
||||||
|
"--models cannot be combined with --self",
|
||||||
|
);
|
||||||
|
expect(process.exitCode).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("cycles project package overrides in config local mode", async () => {
|
it("cycles project package overrides in config local mode", async () => {
|
||||||
const storage = new InMemorySettingsStorage();
|
const storage = new InMemorySettingsStorage();
|
||||||
storage.withLock("global", () => JSON.stringify({ packages: ["npm:pi-tools"] }));
|
storage.withLock("global", () => JSON.stringify({ packages: ["npm:pi-tools"] }));
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ function model(id: string): Model<"openai-completions"> {
|
|||||||
afterEach(() => vi.restoreAllMocks());
|
afterEach(() => vi.restoreAllMocks());
|
||||||
|
|
||||||
describe("remote catalog provider", () => {
|
describe("remote catalog provider", () => {
|
||||||
it("parses keyed catalogs, sends version headers, and observes the refresh TTL", async () => {
|
it("parses keyed catalogs, sends version headers, observes the refresh TTL, and supports forced refreshes", async () => {
|
||||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||||
new Response(JSON.stringify({ dynamic: model("dynamic") }), {
|
async () =>
|
||||||
status: 200,
|
new Response(JSON.stringify({ dynamic: model("dynamic") }), {
|
||||||
headers: { "content-type": "application/json" },
|
status: 200,
|
||||||
}),
|
headers: { "content-type": "application/json" },
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const provider = withRemoteCatalog(
|
const provider = withRemoteCatalog(
|
||||||
createProvider({
|
createProvider({
|
||||||
@@ -62,10 +63,20 @@ describe("remote catalog provider", () => {
|
|||||||
},
|
},
|
||||||
allowNetwork: true,
|
allowNetwork: true,
|
||||||
});
|
});
|
||||||
|
await provider.refreshModels?.({
|
||||||
|
credential: { type: "api_key" },
|
||||||
|
store: {
|
||||||
|
read: () => store.read(provider.id),
|
||||||
|
write: (entry) => store.write(provider.id, entry),
|
||||||
|
delete: () => store.delete(provider.id),
|
||||||
|
},
|
||||||
|
allowNetwork: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static", "dynamic"]);
|
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static", "dynamic"]);
|
||||||
expect((await store.read(provider.id))?.models.map((entry) => entry.id)).toEqual(["dynamic"]);
|
expect((await store.read(provider.id))?.models.map((entry) => entry.id)).toEqual(["dynamic"]);
|
||||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||||
expect(fetchSpy.mock.calls[0]?.[1]?.headers).toMatchObject({
|
expect(fetchSpy.mock.calls[0]?.[1]?.headers).toMatchObject({
|
||||||
"User-Agent": expect.stringContaining(`pi/${VERSION}`),
|
"User-Agent": expect.stringContaining(`pi/${VERSION}`),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.80.8] - 2026-07-16
|
||||||
|
|
||||||
## [0.80.7] - 2026-07-14
|
## [0.80.7] - 2026-07-14
|
||||||
|
|
||||||
## [0.80.6] - 2026-07-09
|
## [0.80.6] - 2026-07-09
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@earendil-works/pi-orchestrator",
|
"name": "@earendil-works/pi-orchestrator",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"description": "experimental orchestrator package for pi",
|
"description": "experimental orchestrator package for pi",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"node": ">=22.19.0"
|
"node": ">=22.19.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "^0.80.7"
|
"@earendil-works/pi-coding-agent": "^0.80.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"shx": "0.4.0"
|
"shx": "0.4.0"
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.80.8] - 2026-07-16
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed terminal output to normalize tab characters consistently ([#6697](https://github.com/earendil-works/pi-mono/pull/6697) by [@xz-dev](https://github.com/xz-dev)).
|
||||||
|
|
||||||
## [0.80.7] - 2026-07-14
|
## [0.80.7] - 2026-07-14
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@earendil-works/pi-tui",
|
"name": "@earendil-works/pi-tui",
|
||||||
"version": "0.80.7",
|
"version": "0.80.8",
|
||||||
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import {
|
||||||
|
mkdtempSync,
|
||||||
|
readFileSync,
|
||||||
|
readdirSync,
|
||||||
|
rmSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join, resolve } from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { isDeepStrictEqual } from "node:util";
|
||||||
|
|
||||||
|
const CATALOG_SCHEMA_VERSION = 1;
|
||||||
|
const CATALOG_PREFIX = `models/v${CATALOG_SCHEMA_VERSION}`;
|
||||||
|
const CATALOG_INDEX_KEY = `${CATALOG_PREFIX}/index.json`;
|
||||||
|
// Bump this only when generated model metadata requires behavior unavailable in older pi clients.
|
||||||
|
const MINIMUM_PI_VERSION = "0.80.7";
|
||||||
|
const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
||||||
|
const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
||||||
|
const INDEX_CACHE_CONTROL = "no-store";
|
||||||
|
const REQUIRED_PROVIDERS = ["anthropic", "openai", "openrouter"];
|
||||||
|
const MINIMUM_MODEL_COUNT = 500;
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const options = {
|
||||||
|
input: undefined,
|
||||||
|
bucket: undefined,
|
||||||
|
endpoint: undefined,
|
||||||
|
sourceCommit: undefined,
|
||||||
|
dryRun: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < args.length; index++) {
|
||||||
|
const arg = args[index];
|
||||||
|
if (arg === "--dry-run") {
|
||||||
|
options.dryRun = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--input" || arg === "--bucket" || arg === "--endpoint" || arg === "--source-commit") {
|
||||||
|
const value = args[++index];
|
||||||
|
if (!value) throw new Error(`${arg} requires a value`);
|
||||||
|
options[
|
||||||
|
{
|
||||||
|
"--input": "input",
|
||||||
|
"--bucket": "bucket",
|
||||||
|
"--endpoint": "endpoint",
|
||||||
|
"--source-commit": "sourceCommit",
|
||||||
|
}[arg]
|
||||||
|
] = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.input) throw new Error("--input is required");
|
||||||
|
if (!options.dryRun && !options.bucket) throw new Error("--bucket is required when publishing");
|
||||||
|
if (!options.dryRun && !options.endpoint) throw new Error("--endpoint is required when publishing");
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJson(path) {
|
||||||
|
return JSON.parse(readFileSync(path, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value) {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBundle(inputDir) {
|
||||||
|
const modelsPath = join(inputDir, "models.json");
|
||||||
|
const providerIndexPath = join(inputDir, "providers.json");
|
||||||
|
const providersDir = join(inputDir, "providers");
|
||||||
|
const modelsBytes = readFileSync(modelsPath);
|
||||||
|
const models = JSON.parse(modelsBytes.toString("utf8"));
|
||||||
|
const providerIds = readJson(providerIndexPath);
|
||||||
|
|
||||||
|
if (!isRecord(models)) throw new Error("models.json must contain an object");
|
||||||
|
if (!Array.isArray(providerIds) || !providerIds.every((value) => typeof value === "string")) {
|
||||||
|
throw new Error("providers.json must contain an array of provider IDs");
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedProviderIds = Object.keys(models).sort();
|
||||||
|
if (!isDeepStrictEqual(providerIds, expectedProviderIds)) {
|
||||||
|
throw new Error("providers.json does not match the sorted providers in models.json");
|
||||||
|
}
|
||||||
|
for (const providerId of REQUIRED_PROVIDERS) {
|
||||||
|
if (!Object.hasOwn(models, providerId)) throw new Error(`Required provider is missing: ${providerId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let modelCount = 0;
|
||||||
|
for (const providerId of providerIds) {
|
||||||
|
const providerModels = models[providerId];
|
||||||
|
if (!isRecord(providerModels)) throw new Error(`Provider catalog must be an object: ${providerId}`);
|
||||||
|
const providerFile = readJson(join(providersDir, `${providerId}.json`));
|
||||||
|
if (!isDeepStrictEqual(providerFile, providerModels)) {
|
||||||
|
throw new Error(`Provider shard does not match models.json: ${providerId}`);
|
||||||
|
}
|
||||||
|
for (const [modelId, model] of Object.entries(providerModels)) {
|
||||||
|
if (!isRecord(model) || model.id !== modelId || model.provider !== providerId) {
|
||||||
|
throw new Error(`Invalid model entry: ${providerId}/${modelId}`);
|
||||||
|
}
|
||||||
|
modelCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shardFiles = readdirSync(providersDir).filter((name) => name.endsWith(".json")).sort();
|
||||||
|
const expectedShardFiles = providerIds.map((providerId) => `${providerId}.json`).sort();
|
||||||
|
if (!isDeepStrictEqual(shardFiles, expectedShardFiles)) {
|
||||||
|
throw new Error("Provider shard files do not match providers.json");
|
||||||
|
}
|
||||||
|
if (modelCount < MINIMUM_MODEL_COUNT) {
|
||||||
|
throw new Error(`Refusing to publish only ${modelCount} models; expected at least ${MINIMUM_MODEL_COUNT}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const digest = createHash("sha256").update(modelsBytes).digest("hex");
|
||||||
|
return {
|
||||||
|
modelsPath,
|
||||||
|
providerIndexPath,
|
||||||
|
providersDir,
|
||||||
|
providerIds,
|
||||||
|
providerCount: providerIds.length,
|
||||||
|
modelCount,
|
||||||
|
revision: `sha256-${digest}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitSourceCommit() {
|
||||||
|
const result = spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" });
|
||||||
|
if (result.status !== 0) throw new Error(`Unable to determine source commit: ${result.stderr.trim()}`);
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function aws(args, { allowNotFound = false } = {}) {
|
||||||
|
const result = spawnSync("aws", args, {
|
||||||
|
encoding: "utf8",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
AWS_DEFAULT_REGION: process.env.AWS_DEFAULT_REGION || "auto",
|
||||||
|
AWS_EC2_METADATA_DISABLED: "true",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
if (result.status === 0) return true;
|
||||||
|
const message = `${result.stdout}\n${result.stderr}`.trim();
|
||||||
|
if (allowNotFound && /(?:404|NoSuchKey|Not Found)/i.test(message)) return false;
|
||||||
|
throw new Error(`aws ${args.slice(0, 2).join(" ")} failed:\n${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadIndex(bucket, endpoint, outputPath) {
|
||||||
|
return aws(
|
||||||
|
[
|
||||||
|
"s3",
|
||||||
|
"cp",
|
||||||
|
`s3://${bucket}/${CATALOG_INDEX_KEY}`,
|
||||||
|
outputPath,
|
||||||
|
"--endpoint-url",
|
||||||
|
endpoint,
|
||||||
|
"--only-show-errors",
|
||||||
|
],
|
||||||
|
{ allowNotFound: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadJson(bucket, endpoint, sourcePath, key, cacheControl) {
|
||||||
|
aws([
|
||||||
|
"s3",
|
||||||
|
"cp",
|
||||||
|
sourcePath,
|
||||||
|
`s3://${bucket}/${key}`,
|
||||||
|
"--endpoint-url",
|
||||||
|
endpoint,
|
||||||
|
"--content-type",
|
||||||
|
JSON_CONTENT_TYPE,
|
||||||
|
"--cache-control",
|
||||||
|
cacheControl,
|
||||||
|
"--only-show-errors",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateIndex(index) {
|
||||||
|
if (!isRecord(index) || index.schemaVersion !== CATALOG_SCHEMA_VERSION) {
|
||||||
|
throw new Error(`Existing ${CATALOG_INDEX_KEY} has an unsupported schema`);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(index.catalogs)) throw new Error(`Existing ${CATALOG_INDEX_KEY} has no catalogs array`);
|
||||||
|
for (const catalog of index.catalogs) {
|
||||||
|
if (
|
||||||
|
!isRecord(catalog) ||
|
||||||
|
typeof catalog.minimumPiVersion !== "string" ||
|
||||||
|
typeof catalog.revision !== "string"
|
||||||
|
) {
|
||||||
|
throw new Error(`Existing ${CATALOG_INDEX_KEY} contains an invalid catalog entry`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparePiVersions(left, right) {
|
||||||
|
const leftParts = left.split(".").map(Number);
|
||||||
|
const rightParts = right.split(".").map(Number);
|
||||||
|
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index++) {
|
||||||
|
const difference = (leftParts[index] || 0) - (rightParts[index] || 0);
|
||||||
|
if (difference !== 0) return difference;
|
||||||
|
}
|
||||||
|
return left.localeCompare(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIndex(existingIndex, publication) {
|
||||||
|
const entry = {
|
||||||
|
minimumPiVersion: MINIMUM_PI_VERSION,
|
||||||
|
revision: publication.revision,
|
||||||
|
sourceCommit: publication.sourceCommit,
|
||||||
|
publishedAt: new Date().toISOString(),
|
||||||
|
providerCount: publication.providerCount,
|
||||||
|
modelCount: publication.modelCount,
|
||||||
|
};
|
||||||
|
const catalogs = (existingIndex?.catalogs || [])
|
||||||
|
.filter((catalog) => catalog.minimumPiVersion !== MINIMUM_PI_VERSION)
|
||||||
|
.concat(entry)
|
||||||
|
.sort((left, right) => comparePiVersions(left.minimumPiVersion, right.minimumPiVersion));
|
||||||
|
return {
|
||||||
|
schemaVersion: CATALOG_SCHEMA_VERSION,
|
||||||
|
defaultRevision: publication.revision,
|
||||||
|
catalogs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const options = parseArgs(process.argv.slice(2));
|
||||||
|
const inputDir = resolve(options.input);
|
||||||
|
const bundle = validateBundle(inputDir);
|
||||||
|
const publication = {
|
||||||
|
schemaVersion: CATALOG_SCHEMA_VERSION,
|
||||||
|
minimumPiVersion: MINIMUM_PI_VERSION,
|
||||||
|
revision: bundle.revision,
|
||||||
|
sourceCommit: options.sourceCommit || gitSourceCommit(),
|
||||||
|
providerCount: bundle.providerCount,
|
||||||
|
modelCount: bundle.modelCount,
|
||||||
|
};
|
||||||
|
writeFileSync(join(inputDir, "publication.json"), `${JSON.stringify(publication, null, 2)}\n`);
|
||||||
|
|
||||||
|
console.log(JSON.stringify(publication, null, 2));
|
||||||
|
if (options.dryRun) {
|
||||||
|
console.log(`Validated model catalog at ${inputDir}; no objects uploaded.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const temporaryDir = mkdtempSync(join(tmpdir(), "pi-model-catalog-"));
|
||||||
|
try {
|
||||||
|
const currentIndexPath = join(temporaryDir, "index-current.json");
|
||||||
|
const hasCurrentIndex = downloadIndex(options.bucket, options.endpoint, currentIndexPath);
|
||||||
|
const currentIndex = hasCurrentIndex ? validateIndex(readJson(currentIndexPath)) : undefined;
|
||||||
|
const currentEntry = currentIndex?.catalogs.find(
|
||||||
|
(catalog) => catalog.minimumPiVersion === MINIMUM_PI_VERSION,
|
||||||
|
);
|
||||||
|
if (currentIndex?.defaultRevision === bundle.revision && currentEntry?.revision === bundle.revision) {
|
||||||
|
console.log(`Model catalog ${bundle.revision} is already current; no objects uploaded.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const revisionPrefix = `${CATALOG_PREFIX}/revisions/${bundle.revision}`;
|
||||||
|
uploadJson(options.bucket, options.endpoint, bundle.modelsPath, `${revisionPrefix}/models.json`, IMMUTABLE_CACHE_CONTROL);
|
||||||
|
uploadJson(
|
||||||
|
options.bucket,
|
||||||
|
options.endpoint,
|
||||||
|
bundle.providerIndexPath,
|
||||||
|
`${revisionPrefix}/providers.json`,
|
||||||
|
IMMUTABLE_CACHE_CONTROL,
|
||||||
|
);
|
||||||
|
for (const providerId of bundle.providerIds) {
|
||||||
|
uploadJson(
|
||||||
|
options.bucket,
|
||||||
|
options.endpoint,
|
||||||
|
join(bundle.providersDir, `${providerId}.json`),
|
||||||
|
`${revisionPrefix}/providers/${providerId}.json`,
|
||||||
|
IMMUTABLE_CACHE_CONTROL,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextIndex = buildIndex(currentIndex, publication);
|
||||||
|
const nextIndexPath = join(temporaryDir, "index-next.json");
|
||||||
|
writeFileSync(nextIndexPath, `${JSON.stringify(nextIndex, null, 2)}\n`);
|
||||||
|
uploadJson(options.bucket, options.endpoint, nextIndexPath, CATALOG_INDEX_KEY, INDEX_CACHE_CONTROL);
|
||||||
|
console.log(`Published ${bundle.revision} to s3://${options.bucket}/${revisionPrefix}`);
|
||||||
|
} finally {
|
||||||
|
rmSync(temporaryDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error instanceof Error ? error.message : error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user