@oh-my-pi/pi-ai 15.9.0 → 15.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/dist/types/auth-broker/index.d.ts +1 -0
- package/dist/types/auth-broker/remote-store.d.ts +5 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
- package/dist/types/auth-broker/types.d.ts +2 -0
- package/dist/types/auth-storage.d.ts +23 -0
- package/dist/types/provider-models/openai-compat.d.ts +28 -1
- package/dist/types/providers/openai-completions-compat.d.ts +3 -1
- package/dist/types/providers/openai-responses-shared.d.ts +6 -1
- package/dist/types/providers/vision-guard.d.ts +13 -1
- package/dist/types/types.d.ts +19 -1
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/dist/types/utils/oauth/xiaomi.d.ts +10 -4
- package/package.json +2 -2
- package/src/auth-broker/index.ts +1 -0
- package/src/auth-broker/remote-store.ts +14 -0
- package/src/auth-broker/snapshot-cache.ts +174 -0
- package/src/auth-broker/types.ts +3 -0
- package/src/auth-storage.ts +240 -49
- package/src/models.json +96 -13
- package/src/provider-models/descriptors.ts +9 -0
- package/src/provider-models/openai-compat.ts +69 -17
- package/src/providers/anthropic.ts +0 -1
- package/src/providers/azure-openai-responses.ts +1 -1
- package/src/providers/openai-completions-compat.ts +31 -11
- package/src/providers/openai-completions.ts +15 -12
- package/src/providers/openai-responses-shared.ts +160 -105
- package/src/providers/openai-responses.ts +1 -1
- package/src/providers/vision-guard.ts +22 -1
- package/src/stream.ts +3 -0
- package/src/types.ts +21 -0
- package/src/usage/claude.ts +1 -1
- package/src/utils/oauth/index.ts +18 -0
- package/src/utils/oauth/types.ts +3 -0
- package/src/utils/oauth/xiaomi.ts +72 -17
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.9.2] - 2026-06-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added an AES-256-GCM auth-broker snapshot cache module and `RemoteAuthCredentialStoreOptions.onSnapshot` so broker clients can persist broker-sourced full snapshots without blocking startup on every run.
|
|
10
|
+
- Added `Model.omitMaxOutputTokens` so providers (notably Ollama proxies fronting cloud catalogs) can suppress `max_output_tokens` (Responses) and `max_tokens`/`max_completion_tokens` (Completions) on the wire while still using the catalog `maxTokens` for local budgeting. Without it, `applyCommonResponsesSamplingParams` unconditionally sent the catalog cap and HTTP-400'd against upstream APIs whose true output limit was unknown to OMP. ([#1881](https://github.com/can1357/oh-my-pi/issues/1881))
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Changed usage-ranked OAuth credential selection to pick deterministic session-sticky weighted buckets instead of always choosing the top-ranked account, capping the best account at 2x the baseline session likelihood while keeping equal-priority accounts evenly balanced.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed parallel `function_call` items on the OpenAI Responses API losing arguments on every call except the last when the upstream server interleaves their stream events (observed against llama.cpp and other local Responses-compat hosts). `processResponsesStream` no longer routes `function_call_arguments.{delta,done}`, `output_item.done`, content_part/text/refusal/reasoning events through a singleton `currentItem`/`currentBlock` reference; it now tracks every open item in registries keyed by `output_index` and `item_id` so each event is folded into the matching block and the emitted `toolcall_end` carries the correct `contentIndex`. ([#1880](https://github.com/can1357/oh-my-pi/issues/1880))
|
|
19
|
+
|
|
20
|
+
## [15.9.1] - 2026-06-04
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Added regional Xiaomi Token Plan login/provider entries (`xiaomi-token-plan-sgp`, `xiaomi-token-plan-ams`, `xiaomi-token-plan-cn`) so `omp login` can store token-plan keys against the selected region. ([#1846](https://github.com/can1357/oh-my-pi/issues/1846))
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Removed the `context-1m-2025-08-07` (1M long-context) beta from the Anthropic agent request headers, the OAuth model-discovery header, and the Claude usage-API header. Sending it caused subscription/OAuth requests without long-context credits to fail with `429 Usage credits are required for long context requests`, breaking Sonnet. The remaining betas are unchanged.
|
|
29
|
+
- Fixed Kimi K2.x `maxTokens` on Fireworks and Fire Pass (`fireworks/kimi-k2.5`, `fireworks/kimi-k2.6`, `firepass/kimi-k2.6-turbo`) being inherited from Fireworks `/v1/models` discovery (`max_completion_tokens: 65536`) rather than the published Kimi-on-Fireworks output budget, which let callers (and the openai-completions default-injection safety net) ship a budget the router cannot honor and made runaway reasoning traces more likely. The Fireworks resolver now clamps every Kimi K2.x id (public catalog ids and the canonical `accounts/fireworks/{models,routers}/kimi-k2…` wire form) to 32,768 output tokens, and the generator applies the same cap as a post-processing safety net so the `firepass` static fallback and the bundled `fireworks` entries stay in sync across regens. ([#1849](https://github.com/can1357/oh-my-pi/issues/1849))
|
|
30
|
+
- Fixed Xiaomi Token Plan MiMo OpenAI-compatible tool-call continuations omitting required `reasoning_content` replay. ([#1846](https://github.com/can1357/oh-my-pi/issues/1846))
|
|
31
|
+
- Fixed Anthropic prompt caching for OpenAI-compatible Claude proxies by honoring `compat.cacheControlFormat: "anthropic"` outside OpenRouter. ([#1845](https://github.com/can1357/oh-my-pi/issues/1845))
|
|
32
|
+
- Fixed Moonshot Kimi K2.6 silently pausing for many seconds between tool calls because the server discarded the `reasoning_content` that omp was already sending with every assistant tool-call replay. The K2.6 `thinking` parameter takes an extra `keep` field whose default (`null`) ignores historical reasoning, so K2.6 had to re-derive its full chain-of-thought from the user prompt on every iteration of the agent loop. The Moonshot direct (`api.moonshot.ai`) and Kimi Code (`api.kimi.com`) wire bodies now send `thinking: { type: "enabled", keep: "all" }` for `kimi-k2.6` requests with reasoning enabled, matching Moonshot's documented best practice for multi-step tool-calling agents. The flag is gated on the K2.6 id and the two native hosts because earlier Moonshot models (K2.5 and below) 400 on the unknown field and every Kimi gateway (OpenRouter, OpenCode, Kilo, Fireworks, …) speaks its own thinking shape. ([#1838](https://github.com/can1357/oh-my-pi/issues/1838))
|
|
33
|
+
- Fixed Alibaba DashScope (Bailian) compatible-mode endpoint `400 InternalError.Algo.InvalidParameter: The provided messages input is invalid. The error info is [Unexpected item type in content.]` when a screenshot or other image-producing tool result was folded into a known text-only Qwen turn (e.g. `qwen3.7-max`, `qwen-max`, `qwen3-coder-*`) hosted at `dashscope.aliyuncs.com/compatible-mode/v1`. `convertMessages` in `openai-completions` no longer forwards `image_url` content parts for those text-only id families even when a misconfigured custom provider claims `input: ["text", "image"]`; multimodal compatible-mode ids such as `qwen3.7-plus` and `qwen-vl-max` still rely on the catalog `input` field. The tool-result branch and the user-content branch both fall back to the standard `[image omitted: model does not support vision]` placeholder for text-only ids so the model still sees the attachment intent. ([#1859](https://github.com/can1357/oh-my-pi/issues/1859))
|
|
34
|
+
|
|
5
35
|
## [15.9.0] - 2026-06-04
|
|
6
36
|
|
|
7
37
|
### Fixed
|
|
@@ -18,6 +48,7 @@
|
|
|
18
48
|
- Fixed MiniMax Coding Plan China login opening the international `platform.minimax.io` subscription page instead of the China `platform.minimaxi.com` page.
|
|
19
49
|
|
|
20
50
|
## [15.8.0] - 2026-06-02
|
|
51
|
+
|
|
21
52
|
### Added
|
|
22
53
|
|
|
23
54
|
- Added `AnthropicMessagesClient` and related Anthropic wire types/errors via `anthropic-client` export so callers can build a standalone Anthropic Messages client without depending on `@anthropic-ai/sdk`
|
|
@@ -16,6 +16,11 @@ export interface RemoteAuthCredentialStoreOptions {
|
|
|
16
16
|
* to long-poll permanently when the broker returns 404. Default `true`.
|
|
17
17
|
*/
|
|
18
18
|
streamSnapshots?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Called after broker-sourced full snapshots are applied. The constructor's
|
|
21
|
+
* initial snapshot intentionally does not trigger this hook.
|
|
22
|
+
*/
|
|
23
|
+
onSnapshot?: (snapshot: SnapshotResponse, generation: number) => void;
|
|
19
24
|
}
|
|
20
25
|
export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
21
26
|
#private;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SnapshotResponse } from "./types";
|
|
2
|
+
export interface ReadAuthBrokerSnapshotCacheOptions {
|
|
3
|
+
path: string;
|
|
4
|
+
token: string;
|
|
5
|
+
url: string;
|
|
6
|
+
ttlMs: number;
|
|
7
|
+
/** Override clock for deterministic tests. */
|
|
8
|
+
now?: () => number;
|
|
9
|
+
}
|
|
10
|
+
export interface WriteAuthBrokerSnapshotCacheOptions {
|
|
11
|
+
path: string;
|
|
12
|
+
token: string;
|
|
13
|
+
url: string;
|
|
14
|
+
snapshot: SnapshotResponse;
|
|
15
|
+
}
|
|
16
|
+
export declare function readAuthBrokerSnapshotCache(opts: ReadAuthBrokerSnapshotCacheOptions): Promise<SnapshotResponse | null>;
|
|
17
|
+
export declare function writeAuthBrokerSnapshotCache(opts: WriteAuthBrokerSnapshotCacheOptions): Promise<void>;
|
|
@@ -96,6 +96,8 @@ export declare const DEFAULT_AUTH_BROKER_BIND = "127.0.0.1:8765";
|
|
|
96
96
|
export declare const DEFAULT_REFRESH_SKEW_MS: number;
|
|
97
97
|
/** Default broker refresh-loop cadence. */
|
|
98
98
|
export declare const DEFAULT_REFRESH_INTERVAL_MS = 60000;
|
|
99
|
+
/** Default freshness window for the encrypted local broker snapshot cache. */
|
|
100
|
+
export declare const DEFAULT_SNAPSHOT_CACHE_TTL_MS: number;
|
|
99
101
|
/** Keepalive cadence for `GET /v1/snapshot/stream` SSE comments. */
|
|
100
102
|
export declare const DEFAULT_STREAM_KEEPALIVE_MS = 20000;
|
|
101
103
|
/**
|
|
@@ -377,11 +377,25 @@ type AuthApiKeyOptions = {
|
|
|
377
377
|
*/
|
|
378
378
|
export interface OAuthAccess {
|
|
379
379
|
accessToken: string;
|
|
380
|
+
credentialId?: number;
|
|
380
381
|
accountId?: string;
|
|
381
382
|
email?: string;
|
|
382
383
|
projectId?: string;
|
|
383
384
|
enterpriseUrl?: string;
|
|
384
385
|
}
|
|
386
|
+
export interface OAuthAccessFailure {
|
|
387
|
+
credentialId?: number;
|
|
388
|
+
accountId?: string;
|
|
389
|
+
email?: string;
|
|
390
|
+
projectId?: string;
|
|
391
|
+
enterpriseUrl?: string;
|
|
392
|
+
error: string;
|
|
393
|
+
}
|
|
394
|
+
export type OAuthAccessResolution = ({
|
|
395
|
+
ok: true;
|
|
396
|
+
} & OAuthAccess) | ({
|
|
397
|
+
ok: false;
|
|
398
|
+
} & OAuthAccessFailure);
|
|
385
399
|
export interface InvalidateCredentialMatchingOptions {
|
|
386
400
|
signal?: AbortSignal;
|
|
387
401
|
sessionId?: string;
|
|
@@ -622,6 +636,15 @@ export declare class AuthStorage {
|
|
|
622
636
|
* OAuth with an explicit API key.
|
|
623
637
|
*/
|
|
624
638
|
getOAuthAccess(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<OAuthAccess | undefined>;
|
|
639
|
+
/**
|
|
640
|
+
* Resolve every stored OAuth credential for `provider` independently.
|
|
641
|
+
*
|
|
642
|
+
* Refreshes credentials through the same broker/local path as
|
|
643
|
+
* {@link AuthStorage.getOAuthAccess}, but does not rank, round-robin, or
|
|
644
|
+
* stop after the first usable account. Intended for diagnostics that must
|
|
645
|
+
* exercise each stored account exactly once.
|
|
646
|
+
*/
|
|
647
|
+
getOAuthAccesses(provider: string, options?: AuthApiKeyOptions): Promise<OAuthAccessResolution[]>;
|
|
625
648
|
invalidateCredentialMatching(provider: string, apiKey: string, options?: InvalidateCredentialMatchingOptions): Promise<boolean>;
|
|
626
649
|
invalidateCredentialMatching(provider: string, apiKey: string, signal?: AbortSignal): Promise<boolean>;
|
|
627
650
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ModelManagerOptions } from "../model-manager";
|
|
2
|
-
import type { Api, Model } from "../types";
|
|
2
|
+
import type { Api, Model, Provider } from "../types";
|
|
3
3
|
export interface ModelsDevModel {
|
|
4
4
|
id?: string;
|
|
5
5
|
name?: string;
|
|
@@ -110,6 +110,27 @@ export interface ZhipuCodingPlanModelManagerConfig {
|
|
|
110
110
|
baseUrl?: string;
|
|
111
111
|
}
|
|
112
112
|
export declare function zhipuCodingPlanModelManagerOptions(config?: ZhipuCodingPlanModelManagerConfig): ModelManagerOptions<"openai-completions">;
|
|
113
|
+
/**
|
|
114
|
+
* Fireworks-published cap for the Kimi K2 family. Fireworks' `/v1/models`
|
|
115
|
+
* envelope generically reports `max_completion_tokens: 65536` for every Kimi
|
|
116
|
+
* deployment, but Kimi K2 (instruct / thinking / turbo) on Fireworks is
|
|
117
|
+
* documented to ship long reasoning traces that should be bounded — capping
|
|
118
|
+
* at 32,768 prevents handing callers a budget the router cannot honor.
|
|
119
|
+
* See https://github.com/can1357/oh-my-pi/issues/1849.
|
|
120
|
+
*/
|
|
121
|
+
export declare const FIREWORKS_KIMI_MAX_TOKENS = 32768;
|
|
122
|
+
/**
|
|
123
|
+
* Returns true for any Kimi K2.x public model id served by Fireworks-backed
|
|
124
|
+
* providers (`fireworks` direct, `firepass` router). Matches both the public
|
|
125
|
+
* catalog id (`kimi-k2.5`, `kimi-k2.6`, `kimi-k2.6-turbo`) and the canonical
|
|
126
|
+
* Fireworks wire id (`accounts/fireworks/{models,routers}/kimi-k2…`).
|
|
127
|
+
*/
|
|
128
|
+
export declare function isFireworksKimiK2ModelId(modelId: string): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Clamp the Kimi K2 family's `maxTokens` to {@link FIREWORKS_KIMI_MAX_TOKENS}
|
|
131
|
+
* on Fireworks-backed providers, leaving every other model untouched.
|
|
132
|
+
*/
|
|
133
|
+
export declare function clampFireworksKimiMaxTokens(modelId: string, candidate: number): number;
|
|
113
134
|
export interface FireworksModelManagerConfig {
|
|
114
135
|
apiKey?: string;
|
|
115
136
|
baseUrl?: string;
|
|
@@ -219,10 +240,16 @@ export interface CloudflareAiGatewayModelManagerConfig {
|
|
|
219
240
|
baseUrl?: string;
|
|
220
241
|
}
|
|
221
242
|
export declare function cloudflareAiGatewayModelManagerOptions(config?: CloudflareAiGatewayModelManagerConfig): ModelManagerOptions<"anthropic-messages">;
|
|
243
|
+
/** Region codes for Xiaomi Token Plan clusters exposed as separate login providers. */
|
|
244
|
+
export type XiaomiTokenPlanRegion = "sgp" | "ams" | "cn";
|
|
245
|
+
/** Configures Xiaomi standard or regional Token Plan OpenAI-compatible model discovery. */
|
|
222
246
|
export interface XiaomiModelManagerConfig {
|
|
223
247
|
apiKey?: string;
|
|
224
248
|
baseUrl?: string;
|
|
249
|
+
providerId?: Provider;
|
|
250
|
+
tokenPlanRegion?: XiaomiTokenPlanRegion;
|
|
225
251
|
}
|
|
252
|
+
/** Builds a Xiaomi model manager, preserving Token Plan region provider ids during discovery. */
|
|
226
253
|
export declare function xiaomiModelManagerOptions(config?: XiaomiModelManagerConfig): ModelManagerOptions<"openai-completions">;
|
|
227
254
|
export interface LiteLLMModelManagerConfig {
|
|
228
255
|
apiKey?: string;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Model, OpenAICompat } from "../types";
|
|
2
2
|
type ResolvedToolStrictMode = NonNullable<OpenAICompat["toolStrictMode"]> | "mixed";
|
|
3
|
-
export type ResolvedOpenAICompat = Required<Omit<OpenAICompat, "openRouterRouting" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode">> & {
|
|
3
|
+
export type ResolvedOpenAICompat = Required<Omit<OpenAICompat, "openRouterRouting" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "cacheControlFormat" | "thinkingKeep">> & {
|
|
4
4
|
openRouterRouting?: OpenAICompat["openRouterRouting"];
|
|
5
5
|
vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
|
|
6
6
|
extraBody?: OpenAICompat["extraBody"];
|
|
7
|
+
cacheControlFormat?: OpenAICompat["cacheControlFormat"];
|
|
8
|
+
thinkingKeep?: OpenAICompat["thinkingKeep"];
|
|
7
9
|
toolStrictMode: ResolvedToolStrictMode;
|
|
8
10
|
};
|
|
9
11
|
/**
|
|
@@ -64,8 +64,13 @@ type CommonSamplingOptions = Pick<StreamOptions, "temperature" | "topP" | "topK"
|
|
|
64
64
|
/**
|
|
65
65
|
* Apply the common `StreamOptions` → Responses sampling-parameter mapping (max output tokens,
|
|
66
66
|
* temperature, top-p/k, min-p, presence/repetition penalties, service tier). Mutates `params`.
|
|
67
|
+
*
|
|
68
|
+
* `max_output_tokens` is suppressed when {@link Model.omitMaxOutputTokens} is `true`, so
|
|
69
|
+
* proxies (notably Ollama) that forward to upstream APIs with an unknown output-token cap
|
|
70
|
+
* can let the upstream apply its own default instead of 400-ing on `maxTokens` values that
|
|
71
|
+
* reflect the model's context window rather than the upstream output limit.
|
|
67
72
|
*/
|
|
68
|
-
export declare function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(params: P, options: CommonSamplingOptions | undefined,
|
|
73
|
+
export declare function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(params: P, options: CommonSamplingOptions | undefined, model: Pick<Model, "provider" | "omitMaxOutputTokens">): void;
|
|
69
74
|
type ReasoningOptions = {
|
|
70
75
|
reasoning?: string;
|
|
71
76
|
reasoningSummary?: "auto" | "detailed" | "concise" | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageContent, TextContent } from "../types";
|
|
1
|
+
import type { ImageContent, Model, TextContent } from "../types";
|
|
2
2
|
export declare const NON_VISION_IMAGE_PLACEHOLDER = "[image omitted: model does not support vision]";
|
|
3
3
|
export declare function partitionVisionContent(content: ReadonlyArray<TextContent | ImageContent>, supportsImages: boolean): {
|
|
4
4
|
textBlocks: TextContent[];
|
|
@@ -6,3 +6,15 @@ export declare function partitionVisionContent(content: ReadonlyArray<TextConten
|
|
|
6
6
|
omittedImages: boolean;
|
|
7
7
|
};
|
|
8
8
|
export declare function joinTextWithImagePlaceholder(text: string, omittedImages: boolean): string;
|
|
9
|
+
/**
|
|
10
|
+
* Detect known text-only Qwen models served via Alibaba DashScope's consumer
|
|
11
|
+
* `compatible-mode` endpoint that the upstream chat-completions API rejects
|
|
12
|
+
* multimodal content arrays for. The compatible-mode endpoint also serves
|
|
13
|
+
* multimodal Qwen SKUs without `vl` in the id (e.g. `qwen3.7-plus`), so this
|
|
14
|
+
* guard only covers families verified to be text-only for issue #1859:
|
|
15
|
+
* `qwen*-max` and `qwen*-coder*`.
|
|
16
|
+
*
|
|
17
|
+
* Used as a defensive override in `convertMessages` so a misconfigured custom
|
|
18
|
+
* provider (issue #1859) can't drive the request into an unrecoverable 400.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isDashscopeCompatibleModeTextOnlyQwen(model: Model<"openai-completions">): boolean;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface ThinkingConfig {
|
|
|
48
48
|
/** Provider-specific transport used to encode the selected effort. */
|
|
49
49
|
mode: ThinkingControlMode;
|
|
50
50
|
}
|
|
51
|
-
export type KnownProvider = "alibaba-coding-plan" | "amazon-bedrock" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "gitlab-duo" | "cursor" | "deepseek" | "xai" | "xai-oauth" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "zhipu-coding-plan" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "wafer-pass" | "wafer-serverless" | "zenmux" | "lm-studio";
|
|
51
|
+
export type KnownProvider = "alibaba-coding-plan" | "amazon-bedrock" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "gitlab-duo" | "cursor" | "deepseek" | "xai" | "xai-oauth" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "zhipu-coding-plan" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "wafer-pass" | "wafer-serverless" | "zenmux" | "lm-studio";
|
|
52
52
|
export type Provider = KnownProvider | string;
|
|
53
53
|
import type { Effort } from "./model-thinking";
|
|
54
54
|
/** Token budgets for each thinking level (token-based providers only) */
|
|
@@ -421,6 +421,8 @@ export interface UserMessage {
|
|
|
421
421
|
content: string | (TextContent | ImageContent)[];
|
|
422
422
|
/** True if the message was injected by the system (e.g., auto-continue). */
|
|
423
423
|
synthetic?: boolean;
|
|
424
|
+
/** True when injected mid-turn as a steer; consumed by the agent's pre-LLM transform to wrap it for emphasis. Never rendered. */
|
|
425
|
+
steering?: boolean;
|
|
424
426
|
/** Who initiated this message for billing/attribution semantics. */
|
|
425
427
|
attribution?: MessageAttribution;
|
|
426
428
|
/** Provider-specific opaque payload used to reconstruct transport-native history. */
|
|
@@ -650,6 +652,8 @@ export interface OpenAICompat {
|
|
|
650
652
|
requiresMistralToolIds?: boolean;
|
|
651
653
|
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "zai" uses thinking: { type: "enabled" | "disabled" } (also used by Moonshot Kimi), "qwen" uses top-level enable_thinking, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */
|
|
652
654
|
thinkingFormat?: "openai" | "openrouter" | "zai" | "qwen" | "qwen-chat-template";
|
|
655
|
+
/** Optional `thinking.keep` value for Z.ai/Moonshot-style thinking params. Set false to suppress auto-detected keep. Default: auto-detected. */
|
|
656
|
+
thinkingKeep?: "all" | false;
|
|
653
657
|
/** Which reasoning content field to emit on assistant messages. Default: auto-detected. */
|
|
654
658
|
reasoningContentField?: "reasoning_content" | "reasoning" | "reasoning_text";
|
|
655
659
|
/** Whether assistant tool-call messages must include reasoning content. Default: false. */
|
|
@@ -681,6 +685,8 @@ export interface OpenAICompat {
|
|
|
681
685
|
vercelGatewayRouting?: VercelGatewayRouting;
|
|
682
686
|
/** Extra fields to include in request body (e.g. gateway routing hints for OpenClaw-style proxies). */
|
|
683
687
|
extraBody?: Record<string, unknown>;
|
|
688
|
+
/** Whether chat-completions payloads should include provider-specific prompt-cache markers. */
|
|
689
|
+
cacheControlFormat?: "anthropic" | undefined;
|
|
684
690
|
/** Whether the provider supports the `strict` field in tool definitions. Default: auto-detected per provider/baseUrl (conservative for unknown providers). */
|
|
685
691
|
supportsStrictMode?: boolean;
|
|
686
692
|
/** Whether tool schemas must be sent either all strict or all non-strict. Undefined keeps the existing per-tool mixed behavior. */
|
|
@@ -757,6 +763,18 @@ export interface Model<TApi extends Api = any> {
|
|
|
757
763
|
premiumMultiplier?: number;
|
|
758
764
|
contextWindow: number;
|
|
759
765
|
maxTokens: number;
|
|
766
|
+
/**
|
|
767
|
+
* When `true`, providers MUST omit `max_output_tokens` (Responses) /
|
|
768
|
+
* `max_tokens` / `max_completion_tokens` (Completions) from the outbound
|
|
769
|
+
* request and let the upstream API decide the per-response cap. `maxTokens`
|
|
770
|
+
* is still used locally for budgeting (compaction, context promotion); only
|
|
771
|
+
* the wire field is suppressed.
|
|
772
|
+
*
|
|
773
|
+
* Use this for proxies (notably Ollama) that forward to a backend whose true
|
|
774
|
+
* output limit OMP cannot discover — sending the wrong value triggers 400s
|
|
775
|
+
* from the upstream provider.
|
|
776
|
+
*/
|
|
777
|
+
omitMaxOutputTokens?: boolean;
|
|
760
778
|
headers?: Record<string, string>;
|
|
761
779
|
/**
|
|
762
780
|
* Streaming transport override. When `"pi-native"`, `streamSimple` routes
|
|
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
|
|
|
7
7
|
email?: string;
|
|
8
8
|
accountId?: string;
|
|
9
9
|
};
|
|
10
|
-
export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "openrouter" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "wafer-pass" | "wafer-serverless" | "vllm" | "xai-oauth" | "xiaomi" | "zenmux" | "zai" | "zhipu-coding-plan";
|
|
10
|
+
export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "openrouter" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "wafer-pass" | "wafer-serverless" | "vllm" | "xai-oauth" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "zai" | "zhipu-coding-plan";
|
|
11
11
|
export type OAuthProviderId = OAuthProvider | (string & {});
|
|
12
12
|
export type OAuthPrompt = {
|
|
13
13
|
message: string;
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* Xiaomi MiMo provides OpenAI-compatible models via
|
|
5
5
|
* https://api.xiaomimimo.com/v1.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 2. User copies their API key
|
|
10
|
-
* 3. User pastes the API key into the CLI
|
|
7
|
+
* Standard Xiaomi login opens the pay-as-you-go API key console. Token Plan
|
|
8
|
+
* login opens plan management so users copy the regional `tp-...` key.
|
|
11
9
|
*/
|
|
12
10
|
import type { OAuthController } from "./types";
|
|
11
|
+
/** Region codes accepted by the Xiaomi Token Plan login flow. */
|
|
12
|
+
export type XiaomiTokenPlanRegion = "sgp" | "ams" | "cn";
|
|
13
13
|
/**
|
|
14
14
|
* Login to Xiaomi MiMo.
|
|
15
15
|
*
|
|
@@ -17,3 +17,9 @@ import type { OAuthController } from "./types";
|
|
|
17
17
|
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
18
18
|
*/
|
|
19
19
|
export declare function loginXiaomi(options: OAuthController): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Login to a regional Xiaomi Token Plan endpoint.
|
|
22
|
+
*
|
|
23
|
+
* Prompts for a token-plan API key and validates it against the selected region.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loginXiaomiTokenPlan(options: OAuthController, region: XiaomiTokenPlanRegion): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "15.9.
|
|
4
|
+
"version": "15.9.3",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@bufbuild/protobuf": "^2.12.0",
|
|
42
|
-
"@oh-my-pi/pi-utils": "15.9.
|
|
42
|
+
"@oh-my-pi/pi-utils": "15.9.3",
|
|
43
43
|
"openai": "^6.39.0",
|
|
44
44
|
"partial-json": "^0.1.7",
|
|
45
45
|
"zod": "4.4.3"
|
package/src/auth-broker/index.ts
CHANGED
|
@@ -73,11 +73,17 @@ export interface RemoteAuthCredentialStoreOptions {
|
|
|
73
73
|
* to long-poll permanently when the broker returns 404. Default `true`.
|
|
74
74
|
*/
|
|
75
75
|
streamSnapshots?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Called after broker-sourced full snapshots are applied. The constructor's
|
|
78
|
+
* initial snapshot intentionally does not trigger this hook.
|
|
79
|
+
*/
|
|
80
|
+
onSnapshot?: (snapshot: SnapshotResponse, generation: number) => void;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
79
84
|
readonly #client: AuthBrokerClient;
|
|
80
85
|
readonly #streamSnapshots: boolean;
|
|
86
|
+
readonly #onSnapshot?: (snapshot: SnapshotResponse, generation: number) => void;
|
|
81
87
|
#snapshot: SnapshotResponse = emptySnapshot();
|
|
82
88
|
#snapshotReceivedAt = Date.now();
|
|
83
89
|
#generation = 0;
|
|
@@ -100,6 +106,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
100
106
|
this.#client = opts.client;
|
|
101
107
|
this.#streamSnapshots = opts.streamSnapshots ?? true;
|
|
102
108
|
this.#applySnapshot(opts.initialSnapshot ?? emptySnapshot(), opts.initialSnapshot?.generation ?? 0);
|
|
109
|
+
this.#onSnapshot = opts.onSnapshot;
|
|
103
110
|
void this.#runBackground();
|
|
104
111
|
}
|
|
105
112
|
|
|
@@ -115,6 +122,13 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
115
122
|
this.#snapshot = snapshot;
|
|
116
123
|
this.#generation = generation;
|
|
117
124
|
this.#snapshotReceivedAt = Date.now();
|
|
125
|
+
const onSnapshot = this.#onSnapshot;
|
|
126
|
+
if (!onSnapshot) return;
|
|
127
|
+
try {
|
|
128
|
+
onSnapshot(snapshot, generation);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logger.debug("auth-broker snapshot callback failed", { error: String(error) });
|
|
131
|
+
}
|
|
118
132
|
}
|
|
119
133
|
|
|
120
134
|
async #runBackground(): Promise<void> {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-GCM encrypted local cache for auth-broker snapshots.
|
|
3
|
+
*
|
|
4
|
+
* The cache is defense-in-depth for at-rest snapshots: a copied cache file is
|
|
5
|
+
* useless without the matching broker bearer token and URL. The token itself is
|
|
6
|
+
* still the trust boundary; a process that can read both the token and this file
|
|
7
|
+
* can decrypt the snapshot.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs/promises";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
12
|
+
import type { SnapshotResponse } from "./types";
|
|
13
|
+
import { snapshotResponseSchema } from "./wire-schemas";
|
|
14
|
+
|
|
15
|
+
const MAGIC = new Uint8Array([0x4f, 0x4d, 0x50, 0x53]); // "OMPS"
|
|
16
|
+
const VERSION = 1;
|
|
17
|
+
const VERSION_OFFSET = MAGIC.byteLength;
|
|
18
|
+
const IV_OFFSET = VERSION_OFFSET + 1;
|
|
19
|
+
const IV_LENGTH = 12;
|
|
20
|
+
const HEADER_LENGTH = IV_OFFSET + IV_LENGTH;
|
|
21
|
+
const AES_ALGORITHM = "AES-GCM";
|
|
22
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
23
|
+
const TEXT_DECODER = new TextDecoder();
|
|
24
|
+
const HEX = "0123456789abcdef";
|
|
25
|
+
|
|
26
|
+
export interface ReadAuthBrokerSnapshotCacheOptions {
|
|
27
|
+
path: string;
|
|
28
|
+
token: string;
|
|
29
|
+
url: string;
|
|
30
|
+
ttlMs: number;
|
|
31
|
+
/** Override clock for deterministic tests. */
|
|
32
|
+
now?: () => number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WriteAuthBrokerSnapshotCacheOptions {
|
|
36
|
+
path: string;
|
|
37
|
+
token: string;
|
|
38
|
+
url: string;
|
|
39
|
+
snapshot: SnapshotResponse;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function readAuthBrokerSnapshotCache(
|
|
43
|
+
opts: ReadAuthBrokerSnapshotCacheOptions,
|
|
44
|
+
): Promise<SnapshotResponse | null> {
|
|
45
|
+
if (opts.ttlMs <= 0) return null;
|
|
46
|
+
let data: Uint8Array;
|
|
47
|
+
try {
|
|
48
|
+
data = await fs.readFile(opts.path);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (isEnoent(error)) return null;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const plaintext = await decryptCachePayload(data, opts.token, opts.url);
|
|
56
|
+
if (!plaintext) return null;
|
|
57
|
+
const parsed: unknown = JSON.parse(TEXT_DECODER.decode(plaintext));
|
|
58
|
+
const result = snapshotResponseSchema.safeParse(parsed);
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
logger.debug("auth-broker snapshot cache schema invalid", { path: opts.path });
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const snapshot = result.data;
|
|
64
|
+
const now = opts.now?.() ?? Date.now();
|
|
65
|
+
if (now - snapshot.generatedAt > opts.ttlMs) return null;
|
|
66
|
+
return snapshot;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.debug("auth-broker snapshot cache read failed", { path: opts.path, error: String(error) });
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function writeAuthBrokerSnapshotCache(opts: WriteAuthBrokerSnapshotCacheOptions): Promise<void> {
|
|
74
|
+
const payload = await encryptCachePayload(opts.snapshot, opts.token, opts.url);
|
|
75
|
+
await fs.mkdir(path.dirname(opts.path), { recursive: true });
|
|
76
|
+
const tmpPath = `${opts.path}.${process.pid}.${randomHex(8)}.tmp`;
|
|
77
|
+
let removeTemp = false;
|
|
78
|
+
try {
|
|
79
|
+
const handle = await fs.open(tmpPath, "wx", 0o600);
|
|
80
|
+
removeTemp = true;
|
|
81
|
+
try {
|
|
82
|
+
await handle.writeFile(payload);
|
|
83
|
+
} finally {
|
|
84
|
+
await handle.close();
|
|
85
|
+
}
|
|
86
|
+
await fs.chmod(tmpPath, 0o600);
|
|
87
|
+
await fs.rename(tmpPath, opts.path);
|
|
88
|
+
removeTemp = false;
|
|
89
|
+
} finally {
|
|
90
|
+
if (removeTemp) await fs.rm(tmpPath, { force: true }).catch(() => {});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function encryptCachePayload(snapshot: SnapshotResponse, token: string, url: string): Promise<Uint8Array> {
|
|
95
|
+
const key = await deriveAesKey(token, ["encrypt"]);
|
|
96
|
+
const iv = new Uint8Array(IV_LENGTH);
|
|
97
|
+
globalThis.crypto.getRandomValues(iv);
|
|
98
|
+
const plaintext = TEXT_ENCODER.encode(JSON.stringify(snapshot));
|
|
99
|
+
const ciphertext = new Uint8Array(
|
|
100
|
+
await globalThis.crypto.subtle.encrypt(
|
|
101
|
+
{
|
|
102
|
+
name: AES_ALGORITHM,
|
|
103
|
+
iv,
|
|
104
|
+
additionalData: TEXT_ENCODER.encode(url),
|
|
105
|
+
},
|
|
106
|
+
key,
|
|
107
|
+
plaintext,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
const payload = new Uint8Array(HEADER_LENGTH + ciphertext.byteLength);
|
|
111
|
+
payload.set(MAGIC, 0);
|
|
112
|
+
payload[VERSION_OFFSET] = VERSION;
|
|
113
|
+
payload.set(iv, IV_OFFSET);
|
|
114
|
+
payload.set(ciphertext, HEADER_LENGTH);
|
|
115
|
+
return payload;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function decryptCachePayload(data: Uint8Array, token: string, url: string): Promise<Uint8Array | null> {
|
|
119
|
+
if (data.byteLength <= HEADER_LENGTH) {
|
|
120
|
+
logger.debug("auth-broker snapshot cache file too short");
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
for (let i = 0; i < MAGIC.byteLength; i++) {
|
|
124
|
+
if (data[i] !== MAGIC[i]) {
|
|
125
|
+
logger.debug("auth-broker snapshot cache magic mismatch");
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (data[VERSION_OFFSET] !== VERSION) {
|
|
130
|
+
logger.debug("auth-broker snapshot cache version mismatch", { version: data[VERSION_OFFSET] });
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const key = await deriveAesKey(token, ["decrypt"]);
|
|
134
|
+
const iv = asStrict(data.subarray(IV_OFFSET, HEADER_LENGTH));
|
|
135
|
+
const ciphertext = asStrict(data.subarray(HEADER_LENGTH));
|
|
136
|
+
try {
|
|
137
|
+
return new Uint8Array(
|
|
138
|
+
await globalThis.crypto.subtle.decrypt(
|
|
139
|
+
{
|
|
140
|
+
name: AES_ALGORITHM,
|
|
141
|
+
iv,
|
|
142
|
+
additionalData: TEXT_ENCODER.encode(url),
|
|
143
|
+
},
|
|
144
|
+
key,
|
|
145
|
+
ciphertext,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
logger.debug("auth-broker snapshot cache decrypt failed", { error: String(error) });
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function deriveAesKey(token: string, usages: Array<"encrypt" | "decrypt">): Promise<CryptoKey> {
|
|
155
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", TEXT_ENCODER.encode(token));
|
|
156
|
+
return globalThis.crypto.subtle.importKey("raw", digest, AES_ALGORITHM, false, usages);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function asStrict(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
160
|
+
if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
|
|
161
|
+
return bytes as Uint8Array<ArrayBuffer>;
|
|
162
|
+
}
|
|
163
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
164
|
+
copy.set(bytes);
|
|
165
|
+
return copy;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function randomHex(byteLength: number): string {
|
|
169
|
+
const bytes = new Uint8Array(byteLength);
|
|
170
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
171
|
+
let out = "";
|
|
172
|
+
for (const byte of bytes) out += HEX[byte >> 4] + HEX[byte & 15];
|
|
173
|
+
return out;
|
|
174
|
+
}
|
package/src/auth-broker/types.ts
CHANGED
|
@@ -117,6 +117,9 @@ export const DEFAULT_REFRESH_SKEW_MS = 5 * 60_000;
|
|
|
117
117
|
/** Default broker refresh-loop cadence. */
|
|
118
118
|
export const DEFAULT_REFRESH_INTERVAL_MS = 60_000;
|
|
119
119
|
|
|
120
|
+
/** Default freshness window for the encrypted local broker snapshot cache. */
|
|
121
|
+
export const DEFAULT_SNAPSHOT_CACHE_TTL_MS = 60 * 60_000;
|
|
122
|
+
|
|
120
123
|
/** Keepalive cadence for `GET /v1/snapshot/stream` SSE comments. */
|
|
121
124
|
export const DEFAULT_STREAM_KEEPALIVE_MS = 20_000;
|
|
122
125
|
|