@oh-my-pi/pi-ai 15.10.0 → 15.10.2
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 +47 -1
- package/dist/types/auth-retry.d.ts +72 -0
- package/dist/types/auth-storage.d.ts +67 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/model-thinking.d.ts +0 -7
- package/dist/types/provider-models/openai-compat.d.ts +5 -0
- package/dist/types/providers/__tests__/google-auth.test.d.ts +1 -0
- package/dist/types/providers/anthropic.d.ts +0 -1
- package/dist/types/providers/transform-messages.d.ts +1 -1
- package/dist/types/stream.d.ts +8 -0
- package/dist/types/types.d.ts +10 -7
- package/dist/types/utils/schema/json-schema-validator.d.ts +8 -0
- package/dist/types/utils/sse-debug.d.ts +0 -5
- package/dist/types/utils/stream-markup-healing.d.ts +2 -0
- package/package.json +2 -2
- package/src/auth-gateway/server.ts +73 -25
- package/src/auth-retry.ts +141 -0
- package/src/auth-storage.ts +153 -4
- package/src/index.ts +1 -0
- package/src/model-thinking.ts +0 -12
- package/src/models.json +411 -39
- package/src/provider-models/descriptors.ts +1 -1
- package/src/provider-models/openai-compat.ts +27 -1
- package/src/providers/__tests__/google-auth.test.ts +144 -0
- package/src/providers/anthropic.ts +71 -37
- package/src/providers/azure-openai-responses.ts +34 -22
- package/src/providers/gitlab-duo.ts +3 -2
- package/src/providers/google-auth.ts +54 -5
- package/src/providers/openai-anthropic-shim.ts +5 -2
- package/src/providers/openai-completions.ts +90 -27
- package/src/providers/openai-responses-server.ts +82 -55
- package/src/providers/openai-responses-shared.ts +19 -5
- package/src/providers/openai-responses.ts +38 -26
- package/src/providers/pi-native-client.ts +4 -1
- package/src/providers/transform-messages.ts +208 -98
- package/src/stream.ts +49 -18
- package/src/types.ts +10 -7
- package/src/utils/http-inspector.ts +4 -2
- package/src/utils/request-debug.ts +12 -7
- package/src/utils/schema/json-schema-validator.ts +19 -1
- package/src/utils/sse-debug.ts +0 -271
- package/src/utils/stream-markup-healing.ts +6 -1
- package/src/utils/validation.ts +26 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.10.2] - 2026-06-08
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added support for `impersonated_service_account` Application Default Credentials (ADC) in Vertex AI to enable chained impersonation without failing via 401 `invalid_client`.
|
|
9
|
+
- Added `AuthStorage.getCredentialOrigin(provider)` (returning a structured `CredentialOrigin` / `CredentialOriginKind`) and `getEnvApiKeyName(provider)`, so callers can render where a provider's auth comes from — runtime override, config, stored OAuth/api-key, env var (with the backing variable name), or fallback resolver — without parsing the prose of `describeCredentialSource`.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Changed `onSseEvent` recording for OpenAI Responses, Azure OpenAI Responses, OpenAI Completions, and Anthropic stream providers to emit reconstructed SSE events from decoded SDK stream items instead of wrapping raw fetch responses
|
|
14
|
+
- Changed OpenAI Completions SSE diagnostics to include `event: "chat.completion.chunk"` in `onSseEvent` records for chunked responses
|
|
15
|
+
- Changed the default Anthropic model in `DEFAULT_MODEL_PER_PROVIDER` from `claude-sonnet-4-6` to `claude-opus-4-6`, so sessions that fall back to the provider default (no configured `default` role, no `--model`, no restored session) now start on Claude Opus 4.6.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed duplicate upstream `tool_call_id` values collapsing distinct tool calls during message transformation, preserving one call/result pairing per emitted tool call before provider replay and keeping generated duplicate IDs distinct after OpenAI/Mistral wire-length caps. ([#2055](https://github.com/can1357/oh-my-pi/issues/2055))
|
|
20
|
+
- Fixed the Anthropic provider retrying persistent account usage/quota limits (e.g. `429 "This request would exceed your account's rate limit"`, `usage_limit_reached`) as if they were transient. Because the error text contains "rate limit", `isProviderRetryableError` matched it and the stream retry loop looped through its 2s/4s/8s backoff (then the `streamSimple` a/b/c policy re-minted the credential and ran the whole thing again) before surfacing the failure — even though the server's `retry-after` parked the account for minutes-to-hours. These errors are now recognized via `isUsageLimitError` and surfaced immediately to the credential-rotation layer, so e.g. `omp dry-balance --bench` reports a rate-limited account as failed at once instead of appearing to hang.
|
|
21
|
+
- Fixed MiniMax-compatible OpenAI-completions hosts losing tool-call argument content when `function.arguments` is streamed as an object across more than one delta. The accumulator added in #1776 wrote `block.partialArgs = rawArgs` per chunk, so every chunk but the last was overwritten — for an `edit` call this surfaced as a tail-slice of the patch text being applied (e.g. a single-line `replace 91..91:` body extending the deletion across the surrounding rows). Chunks are now shallow-merged; for shared string keys, `startsWith` distinguishes cumulative restatements (take the latest) from per-chunk-delta fragments (concatenate). Per-chunk `toolcall_delta` emission for the object branch is suppressed (the previous code emitted `JSON.stringify(rawArgs)` per chunk, which fed downstream concat consumers — `packages/agent/src/proxy.ts`, `openai-chat-server`, `openai-responses-server`, `anthropic-messages-server` — an invalid sequence like `{"input":"a"}{"input":"b"}`); the merged object is flushed instead as a single concat-safe delta in `finishToolCallBlock` before `toolcall_end`, so accumulators reconstruct the args correctly. The single-chunk shape covered by the existing #1776 regression test stays correct end-to-end. ([#2080](https://github.com/can1357/oh-my-pi/issues/2080))
|
|
22
|
+
- Fixed the OpenAI Responses compatibility server misrouting late `toolcall_delta` events for earlier parallel tool calls after a later `toolcall_start`. The encoder now keeps OpenFunctionCall state by content index, allocates output indexes at item start, and closes each tool item by its own `toolcall_end`, preserving deferred MiniMax object-argument flushes for the matching call. ([#2080](https://github.com/can1357/oh-my-pi/issues/2080))
|
|
23
|
+
|
|
24
|
+
## [15.10.1] - 2026-06-07
|
|
25
|
+
|
|
26
|
+
### Breaking Changes
|
|
27
|
+
|
|
28
|
+
- Removed the `onAuthError` option from stream request options and shifted auth retry handling to resolver-based `apiKey` behavior, requiring callers using custom auth-retry hooks to migrate
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Added `ApiKeyResolver` and `ApiKey` auth helpers, including `isApiKeyResolver`, `isAuthRetryableError`, `resolveApiKeyOnce`, and `withAuth`, and exported them from the package root
|
|
33
|
+
- Added support for a function-valued `apiKey` in `SimpleStreamOptions` so a single stream request can refresh or rotate credentials during retry
|
|
34
|
+
- Added `forceRefresh` credential option to `AuthStorage.getApiKey` and `rotateSessionCredential` support for session-level credential rotation after auth failures
|
|
35
|
+
- Added `AuthStorage.resolver(provider, options)` method that builds an `ApiKeyResolver` implementing the a/b/c auth-retry policy directly on the storage instance
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Changed gateway and stream auth flows to share the a/b/c retry policy, refreshing the same session credential first and then switching to a sibling credential on repeated auth failures
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Fixed streaming auth retries to handle `401` and usage-limit errors before replay-unsafe content is emitted, including failures surfaced only via `errorStatus`
|
|
44
|
+
- Fixed tool argument validation to coerce singleton non-string values into arrays when the schema expects an array, preventing Anthropic-compatible models that emit `todo.ops` as an object from getting stuck in repeated validation-error loops. ([#2026](https://github.com/can1357/oh-my-pi/issues/2026))
|
|
45
|
+
- Fixed streaming retries to buffer and suppress partial `start` events from failed auth attempts so only clean retried events are delivered
|
|
46
|
+
- Fixed the HTTP 400 raw-request dumper (`appendRawHttpRequestDumpFor400`) littering the real `~/.omp/logs/http-400-requests` directory during tests. Provider suites exercise the 400 error path with mocked `fetch` responses, which the dumper could not distinguish from genuine failures; it now skips persistence under the Bun test runner (`isBunTestRuntime()`).
|
|
47
|
+
- Fixed Anthropic Opus requests unnecessarily forcing `tool_choice.disable_parallel_tool_use`, allowing Claude Opus to use the provider's default parallel tool-calling behavior again.
|
|
48
|
+
- Fixed parallel `function_call` items losing arguments against llama.cpp's OpenAI Responses endpoint (`/v1/responses`), where every call but the last finalized with `{}` and the agent rejected them with `path: Invalid input: expected string, received undefined`. llama.cpp's `to_json_oaicompat_resp` emits `output_item.added` with only `item.call_id` (no `item.id`, no `output_index`) while the matching `function_call_arguments.delta` carries `item_id: "fc_<call_id>"`. `processResponsesStream` now registers function-call and custom-tool-call items under `item.call_id` as a secondary lookup key (alongside `item.id`/`output_index`) so identifier-deviant hosts route deltas and done events to the right block. ([#2015](https://github.com/can1357/oh-my-pi/issues/2015))
|
|
49
|
+
- Fixed `PI_REQ_DEBUG` response recording truncating the captured body when a streamed response was cancelled mid-flight. The response tee in `wrapResponse` could call `FileRequestDebugResponseLog.close()` from both the `cancel` callback and the resumed `pull` (which observes `done` once the source reader is cancelled); the second caller saw the handle already nulled and returned before the first caller's pending write flushed, so the `.res.log` lost the already-buffered chunk. `close()` now memoizes its flush-and-close promise so every caller awaits the same completion.
|
|
50
|
+
|
|
5
51
|
## [15.10.0] - 2026-06-06
|
|
6
52
|
|
|
7
53
|
### Added
|
|
@@ -2980,4 +3026,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
|
|
|
2980
3026
|
|
|
2981
3027
|
## [0.9.4] - 2025-11-26
|
|
2982
3028
|
|
|
2983
|
-
Initial release with multi-provider LLM support.
|
|
3029
|
+
Initial release with multi-provider LLM support.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context passed to an {@link ApiKeyResolver} on each resolution attempt.
|
|
3
|
+
*
|
|
4
|
+
* The `error`/`lastChance` pair drives the central a/b/c retry policy shared by
|
|
5
|
+
* the streaming ({@link streamSimple}) and non-streaming ({@link withAuth})
|
|
6
|
+
* drivers:
|
|
7
|
+
* - `error === undefined` → **initial resolve** (no force-refresh; cheap, may
|
|
8
|
+
* return a locally-cached not-yet-expired token).
|
|
9
|
+
* - `error !== undefined && !lastChance` → **step (b): refresh the SAME
|
|
10
|
+
* account** (force a token re-mint / await an in-flight broker refresh).
|
|
11
|
+
* - `error !== undefined && lastChance` → **step (c): switch account**
|
|
12
|
+
* (invalidate/usage-limit the current credential and rotate to a sibling).
|
|
13
|
+
*
|
|
14
|
+
* The resolver returns the bearer to send, or `undefined` to stop retrying and
|
|
15
|
+
* surface the last error to the caller.
|
|
16
|
+
*/
|
|
17
|
+
export interface ApiKeyResolveContext {
|
|
18
|
+
/** True on the final retry step — the resolver should rotate to a sibling credential. */
|
|
19
|
+
lastChance: boolean;
|
|
20
|
+
/** The auth error that triggered this re-resolution, or `undefined` on the initial resolve. */
|
|
21
|
+
error: unknown;
|
|
22
|
+
/** Caller cancel signal, threaded into any credential refresh / rotation work. */
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the API key to send for a request, retried through the a/b/c policy
|
|
27
|
+
* described on {@link ApiKeyResolveContext}.
|
|
28
|
+
*/
|
|
29
|
+
export type ApiKeyResolver = (ctx: ApiKeyResolveContext) => Promise<string | undefined> | string | undefined;
|
|
30
|
+
/** A static bearer string, or a {@link ApiKeyResolver} that mints/rotates one. */
|
|
31
|
+
export type ApiKey = string | ApiKeyResolver;
|
|
32
|
+
/** Narrows {@link ApiKey} to its resolver form. */
|
|
33
|
+
export declare function isApiKeyResolver(key: ApiKey | undefined): key is ApiKeyResolver;
|
|
34
|
+
/**
|
|
35
|
+
* Performs the initial resolve of an {@link ApiKey} (`error: undefined`,
|
|
36
|
+
* `lastChance: false`). Static keys pass through unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveApiKeyOnce(key: ApiKey | undefined, signal?: AbortSignal): Promise<string | undefined>;
|
|
39
|
+
/**
|
|
40
|
+
* Classifies whether an error should trigger a credential refresh/rotation
|
|
41
|
+
* retry: a hard `401`, or a rotatable usage-limit ("usage_limit_reached",
|
|
42
|
+
* Codex's "you have hit your ChatGPT usage limit", etc.).
|
|
43
|
+
*/
|
|
44
|
+
export declare function isAuthRetryableError(error: unknown): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The ordered `lastChance` values for the retry steps after the initial
|
|
47
|
+
* attempt fails: `false` → step (b) refresh-same, `true` → step (c) switch.
|
|
48
|
+
* Shared by {@link withAuth} and the streaming retry driver so both run the
|
|
49
|
+
* same policy.
|
|
50
|
+
*/
|
|
51
|
+
export declare const AUTH_RETRY_STEPS: readonly boolean[];
|
|
52
|
+
/** Resolve a single retry step, swallowing resolver failures into `undefined`. */
|
|
53
|
+
export declare function resolveRetryKey(resolver: ApiKeyResolver, lastChance: boolean, error: unknown, signal?: AbortSignal): Promise<string | undefined>;
|
|
54
|
+
/**
|
|
55
|
+
* Runs an auth-protected operation through the central a/b/c retry policy.
|
|
56
|
+
*
|
|
57
|
+
* - A static string key (or any non-resolver) → a single `attempt` with no
|
|
58
|
+
* retry (identical to the legacy static-key path).
|
|
59
|
+
* - A resolver → initial `attempt`, then on a retryable auth error up to two
|
|
60
|
+
* more attempts (refresh-same, then switch). A step is skipped when the
|
|
61
|
+
* resolver returns the same key it just tried or `undefined`; non-auth errors
|
|
62
|
+
* propagate immediately.
|
|
63
|
+
*
|
|
64
|
+
* Used by non-streaming consumers (image generation, web search, completion
|
|
65
|
+
* helpers). The streaming driver in `stream.ts` implements the same policy with
|
|
66
|
+
* its replay-safe buffering machinery.
|
|
67
|
+
*/
|
|
68
|
+
export declare function withAuth<T>(key: ApiKey | undefined, attempt: (key: string) => Promise<T>, opts?: {
|
|
69
|
+
isAuthError?: (error: unknown) => boolean;
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
missingKeyMessage?: string;
|
|
72
|
+
}): Promise<T>;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - `SqliteAuthCredentialStore`: concrete SQLite-backed implementation
|
|
9
9
|
*/
|
|
10
10
|
import { Database } from "bun:sqlite";
|
|
11
|
+
import type { ApiKeyResolver } from "./auth-retry";
|
|
11
12
|
import type { Provider } from "./types";
|
|
12
13
|
import type { CredentialRankingStrategy, UsageLogger, UsageProvider, UsageReport } from "./usage";
|
|
13
14
|
import type { OAuthController, OAuthCredentials, OAuthProviderId } from "./utils/oauth/types";
|
|
@@ -21,6 +22,21 @@ export type OAuthCredential = {
|
|
|
21
22
|
export type AuthCredential = ApiKeyCredential | OAuthCredential;
|
|
22
23
|
export type AuthCredentialEntry = AuthCredential | AuthCredential[];
|
|
23
24
|
export type AuthStorageData = Record<string, AuthCredentialEntry>;
|
|
25
|
+
/**
|
|
26
|
+
* Cascade leg that supplies a provider's active credential, highest precedence
|
|
27
|
+
* first — mirrors {@link AuthStorage.getApiKey}'s resolution order.
|
|
28
|
+
*/
|
|
29
|
+
export type CredentialOriginKind = "runtime" | "config" | "oauth" | "api_key" | "env" | "fallback";
|
|
30
|
+
/**
|
|
31
|
+
* Structured provenance for a provider's auth, for UI that needs a machine
|
|
32
|
+
* tag (the `/login` provider list) rather than the prose of
|
|
33
|
+
* {@link AuthStorage.describeCredentialSource}.
|
|
34
|
+
*/
|
|
35
|
+
export interface CredentialOrigin {
|
|
36
|
+
kind: CredentialOriginKind;
|
|
37
|
+
/** Env var name when `kind === "env"` and a single named variable backs it. */
|
|
38
|
+
envVar?: string;
|
|
39
|
+
}
|
|
24
40
|
/**
|
|
25
41
|
* Serialized representation of AuthStorage for passing to subagent workers.
|
|
26
42
|
* Contains only the essential credential data, not runtime state.
|
|
@@ -366,6 +382,13 @@ type AuthApiKeyOptions = {
|
|
|
366
382
|
* stranding the caller for `timeoutMs * (maxRetries + 1)`.
|
|
367
383
|
*/
|
|
368
384
|
signal?: AbortSignal;
|
|
385
|
+
/**
|
|
386
|
+
* Force a re-mint of the session-preferred OAuth credential's access token,
|
|
387
|
+
* bypassing the not-yet-expired short-circuit. Powers step (b) of the
|
|
388
|
+
* auth-retry policy ("refresh the SAME account") so a locally-cached token
|
|
389
|
+
* that a peer/broker rotated out from under us is replaced before retrying.
|
|
390
|
+
*/
|
|
391
|
+
forceRefresh?: boolean;
|
|
369
392
|
};
|
|
370
393
|
/**
|
|
371
394
|
* Refreshed OAuth access plus identity metadata returned by
|
|
@@ -517,6 +540,15 @@ export declare class AuthStorage {
|
|
|
517
540
|
* silently satisfies xai-oauth and routes around `providers.xai.baseUrl`.
|
|
518
541
|
*/
|
|
519
542
|
hasNonEnvCredential(provider: string): boolean;
|
|
543
|
+
/**
|
|
544
|
+
* Classify where a provider's auth comes from, following the same precedence
|
|
545
|
+
* as {@link AuthStorage.getApiKey}: runtime override → config override →
|
|
546
|
+
* stored credential (api_key before oauth, matching getApiKey) → env var →
|
|
547
|
+
* fallback resolver. Returns undefined when no auth is configured.
|
|
548
|
+
*
|
|
549
|
+
* Compact, structured counterpart to {@link describeCredentialSource}.
|
|
550
|
+
*/
|
|
551
|
+
getCredentialOrigin(provider: string): CredentialOrigin | undefined;
|
|
520
552
|
/**
|
|
521
553
|
* Check if OAuth credentials are configured for a provider.
|
|
522
554
|
*/
|
|
@@ -647,6 +679,41 @@ export declare class AuthStorage {
|
|
|
647
679
|
getOAuthAccesses(provider: string, options?: AuthApiKeyOptions): Promise<OAuthAccessResolution[]>;
|
|
648
680
|
invalidateCredentialMatching(provider: string, apiKey: string, options?: InvalidateCredentialMatchingOptions): Promise<boolean>;
|
|
649
681
|
invalidateCredentialMatching(provider: string, apiKey: string, signal?: AbortSignal): Promise<boolean>;
|
|
682
|
+
/**
|
|
683
|
+
* Rotate away from the session's current credential after a retryable auth
|
|
684
|
+
* error — step (c) of the auth-retry policy. Stateless: looks up the
|
|
685
|
+
* session-sticky credential (no API-key matching needed), applies the
|
|
686
|
+
* storage action for the error class, then clears the sticky so the next
|
|
687
|
+
* {@link AuthStorage.getApiKey} for this session picks a sibling.
|
|
688
|
+
*
|
|
689
|
+
* - usage-limit / account-rate-limit error → {@link AuthStorage.markUsageLimitReached}
|
|
690
|
+
* (temporary block via its own backoff — default plus server usage-report
|
|
691
|
+
* reset; sticky left intact so the next resolve re-ranks around the block).
|
|
692
|
+
* - otherwise (hard 401 / auth failure) → mark the credential suspect (or
|
|
693
|
+
* reload when no broker hook is wired) and block it, then drop the sticky.
|
|
694
|
+
*
|
|
695
|
+
* Returns whether another usable credential of the same type remains.
|
|
696
|
+
*/
|
|
697
|
+
rotateSessionCredential(provider: string, sessionId: string | undefined, options?: {
|
|
698
|
+
error?: unknown;
|
|
699
|
+
signal?: AbortSignal;
|
|
700
|
+
}): Promise<boolean>;
|
|
701
|
+
/**
|
|
702
|
+
* Build an {@link ApiKeyResolver} backed by this storage, implementing the
|
|
703
|
+
* central a/b/c auth-retry policy:
|
|
704
|
+
*
|
|
705
|
+
* - initial (`error: undefined`) → resolve the session credential.
|
|
706
|
+
* - step (b) `!lastChance` → force-refresh the SAME session-sticky credential.
|
|
707
|
+
* - step (c) `lastChance` → rotate to a sibling credential, then re-resolve.
|
|
708
|
+
*
|
|
709
|
+
* Used by web-search providers and other consumers that hold an AuthStorage
|
|
710
|
+
* directly (no ModelRegistry in scope).
|
|
711
|
+
*/
|
|
712
|
+
resolver(provider: string, options?: {
|
|
713
|
+
sessionId?: string;
|
|
714
|
+
baseUrl?: string;
|
|
715
|
+
modelId?: string;
|
|
716
|
+
}): ApiKeyResolver;
|
|
650
717
|
/**
|
|
651
718
|
* Build a redacted snapshot of all loaded credentials for the auth-broker
|
|
652
719
|
* wire. OAuth refresh tokens are replaced with {@link REMOTE_REFRESH_SENTINEL}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./api-registry";
|
|
|
3
3
|
export * from "./auth-broker";
|
|
4
4
|
export { type AuthGatewayBootOptions, type ModelResolver, startAuthGateway } from "./auth-gateway/server";
|
|
5
5
|
export * from "./auth-gateway/types";
|
|
6
|
+
export * from "./auth-retry";
|
|
6
7
|
export * from "./auth-storage";
|
|
7
8
|
export * from "./effort";
|
|
8
9
|
export * from "./model-cache";
|
|
@@ -83,10 +83,3 @@ export declare function hasOpus47ApiRestrictions(modelId: string): boolean;
|
|
|
83
83
|
* @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
|
|
84
84
|
*/
|
|
85
85
|
export declare function supportsMidConversationSystemMessages(modelId: string): boolean;
|
|
86
|
-
/**
|
|
87
|
-
* Claude Opus 4.8 must emit at most one tool call per turn: the Anthropic
|
|
88
|
-
* Messages provider sends `tool_choice.disable_parallel_tool_use = true` for
|
|
89
|
-
* this model. Scoped to exactly 4.8 — earlier and later Opus versions keep
|
|
90
|
-
* Anthropic's default parallel tool-calling.
|
|
91
|
-
*/
|
|
92
|
-
export declare function disablesParallelToolUse(modelId: string): boolean;
|
|
@@ -131,6 +131,11 @@ export declare function isFireworksKimiK2ModelId(modelId: string): boolean;
|
|
|
131
131
|
* on Fireworks-backed providers, leaving every other model untouched.
|
|
132
132
|
*/
|
|
133
133
|
export declare function clampFireworksKimiMaxTokens(modelId: string, candidate: number): number;
|
|
134
|
+
/**
|
|
135
|
+
* Fireworks DeepSeek V4 accepts effort via `reasoning_effort` but rejects the
|
|
136
|
+
* DeepSeek-native binary `thinking` toggle when both are present.
|
|
137
|
+
*/
|
|
138
|
+
export declare function stripFireworksDeepSeekThinkingToggle(model: Model<"openai-completions">, publicModelId: string): Model<"openai-completions">;
|
|
134
139
|
export interface FireworksModelManagerConfig {
|
|
135
140
|
apiKey?: string;
|
|
136
141
|
baseUrl?: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -117,7 +117,6 @@ export type AnthropicClientOptionsArgs = {
|
|
|
117
117
|
hasTools?: boolean;
|
|
118
118
|
thinkingEnabled?: boolean;
|
|
119
119
|
thinkingDisplay?: AnthropicThinkingDisplay;
|
|
120
|
-
onSseEvent?: AnthropicOptions["onSseEvent"];
|
|
121
120
|
fetch?: FetchImpl;
|
|
122
121
|
claudeCodeSessionId?: string;
|
|
123
122
|
};
|
|
@@ -9,4 +9,4 @@ import type { Api, AssistantMessage, Message, Model } from "../types";
|
|
|
9
9
|
* - Injects synthetic "aborted" tool results
|
|
10
10
|
* - Adds a <turn-aborted> guidance marker for the model
|
|
11
11
|
*/
|
|
12
|
-
export declare function transformMessages<TApi extends Api>(messages: Message[], model: Model<TApi>, normalizeToolCallId?: (id: string, model: Model<TApi>, source: AssistantMessage) => string): Message[];
|
|
12
|
+
export declare function transformMessages<TApi extends Api>(messages: Message[], model: Model<TApi>, normalizeToolCallId?: (id: string, model: Model<TApi>, source: AssistantMessage) => string, maxNormalizedToolCallIdLength?: number, duplicateToolCallIdSuffixPrefix?: string): Message[];
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -12,6 +12,14 @@ import { AssistantMessageEventStream } from "./utils/event-stream";
|
|
|
12
12
|
* Checks Bun.env, then cwd/.env, then ~/.env.
|
|
13
13
|
*/
|
|
14
14
|
export declare function getEnvApiKey(provider: string): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Name of the environment variable that backs `getEnvApiKey` for a provider,
|
|
17
|
+
* when that provider maps to a single named variable (e.g. `github-copilot` →
|
|
18
|
+
* `COPILOT_GITHUB_TOKEN`). Returns undefined for providers whose env fallback
|
|
19
|
+
* is computed (multi-var pickers, Vertex ADC / Bedrock probes, …) since no
|
|
20
|
+
* single variable name describes the source.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getEnvApiKeyName(provider: string): string | undefined;
|
|
15
23
|
/**
|
|
16
24
|
* Enumerate every provider that has an env-var fallback for `getEnvApiKey`.
|
|
17
25
|
* Used by `omp auth-broker migrate --include-env` to discover env-sourced keys
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ZodType, z } from "zod/v4";
|
|
2
|
+
import type { ApiKey } from "./auth-retry";
|
|
2
3
|
import type { BedrockOptions } from "./providers/amazon-bedrock";
|
|
3
4
|
import type { AnthropicOptions } from "./providers/anthropic";
|
|
4
5
|
import type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses";
|
|
@@ -159,12 +160,6 @@ export interface StreamOptions {
|
|
|
159
160
|
maxTokens?: number;
|
|
160
161
|
signal?: AbortSignal;
|
|
161
162
|
apiKey?: string;
|
|
162
|
-
/**
|
|
163
|
-
* Called when a provider returns 401 before any replay-unsafe assistant
|
|
164
|
-
* event has been emitted. Returning a different key retries the provider
|
|
165
|
-
* request once.
|
|
166
|
-
*/
|
|
167
|
-
onAuthError?: (provider: string, apiKey: string, error: unknown) => Promise<string | undefined>;
|
|
168
163
|
cacheRetention?: CacheRetention;
|
|
169
164
|
/**
|
|
170
165
|
* Additional headers to include in provider requests.
|
|
@@ -274,7 +269,15 @@ export interface StreamOptions {
|
|
|
274
269
|
/** Cursor exec/MCP tool handlers (cursor-agent only). */
|
|
275
270
|
execHandlers?: CursorExecHandlers;
|
|
276
271
|
}
|
|
277
|
-
export interface SimpleStreamOptions extends StreamOptions {
|
|
272
|
+
export interface SimpleStreamOptions extends Omit<StreamOptions, "apiKey"> {
|
|
273
|
+
/**
|
|
274
|
+
* API key for the request: either a static bearer string, or an
|
|
275
|
+
* {@link ApiKeyResolver} that mints/rotates the key across the central
|
|
276
|
+
* a/b/c auth-retry policy. `streamSimple`/`completeSimple` resolve a
|
|
277
|
+
* resolver to a string before per-provider dispatch, so providers only
|
|
278
|
+
* ever see the resolved {@link StreamOptions.apiKey} string.
|
|
279
|
+
*/
|
|
280
|
+
apiKey?: ApiKey;
|
|
278
281
|
reasoning?: Effort;
|
|
279
282
|
/**
|
|
280
283
|
* Force-disable reasoning for the request even when the model supports it.
|
|
@@ -3,6 +3,14 @@ export interface JsonSchemaValidationIssue {
|
|
|
3
3
|
message: string;
|
|
4
4
|
expectedTypes?: string[];
|
|
5
5
|
keyword?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Marks issues that originate inside a failed `anyOf` / `oneOf` branch.
|
|
8
|
+
* Consumers such as the tool-argument coercion layer use this to avoid
|
|
9
|
+
* applying type repairs (e.g. singleton-array wrapping) that would be
|
|
10
|
+
* authoritative outside of a combinator but are only one candidate
|
|
11
|
+
* branch's expectation here.
|
|
12
|
+
*/
|
|
13
|
+
fromUnionBranch?: boolean;
|
|
6
14
|
}
|
|
7
15
|
export interface JsonSchemaValidationResult {
|
|
8
16
|
success: boolean;
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { ServerSentEvent } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { RawSseEvent } from "../types";
|
|
3
|
-
type FetchFunction = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
4
|
-
type FetchWithPreconnect = FetchFunction & {
|
|
5
|
-
preconnect?: typeof fetch.preconnect;
|
|
6
|
-
};
|
|
7
3
|
type RawSseObserver = (event: RawSseEvent) => void;
|
|
8
4
|
export declare function notifyRawSseEvent(observer: RawSseObserver | undefined, event: ServerSentEvent | RawSseEvent): void;
|
|
9
|
-
export declare function wrapFetchForSseDebug(fetchImpl: FetchWithPreconnect, observer: RawSseObserver | undefined): FetchWithPreconnect;
|
|
10
5
|
export {};
|
|
@@ -75,6 +75,8 @@ export declare class StreamMarkupHealing {
|
|
|
75
75
|
export declare function modelMayLeakKimiToolCalls(provider: string, modelId: string): boolean;
|
|
76
76
|
/** Cheap model/provider gate for DeepSeek DSML envelope leaks. */
|
|
77
77
|
export declare function modelMayLeakDsmlToolCalls(provider: string, modelId: string): boolean;
|
|
78
|
+
/** Cheap model/provider gate for MiniMax plain thinking tag leaks. */
|
|
79
|
+
export declare function modelMayLeakThinkingTags(provider: string, modelId: string): boolean;
|
|
78
80
|
export declare function getStreamMarkupHealingPattern(provider: string, modelId: string, options?: {
|
|
79
81
|
readonly parseThinkingTags?: boolean;
|
|
80
82
|
}): StreamMarkupHealingPattern | undefined;
|
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.10.
|
|
4
|
+
"version": "15.10.2",
|
|
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.10.
|
|
42
|
+
"@oh-my-pi/pi-utils": "15.10.2",
|
|
43
43
|
"openai": "^6.39.0",
|
|
44
44
|
"partial-json": "^0.1.7",
|
|
45
45
|
"zod": "4.4.3"
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* POST /v1/responses → OpenAI Responses in/out
|
|
19
19
|
*/
|
|
20
20
|
import { extractRetryHint, logger } from "@oh-my-pi/pi-utils";
|
|
21
|
+
import type { ApiKeyResolver } from "../auth-retry";
|
|
21
22
|
import type { AuthStorage } from "../auth-storage";
|
|
22
23
|
import { Effort } from "../effort";
|
|
23
24
|
import * as anthropicMessages from "../providers/anthropic-messages-server";
|
|
@@ -340,6 +341,60 @@ async function refreshGatewayApiKeyAfterAuthError(
|
|
|
340
341
|
return storage.getApiKey(provider, sessionId, { modelId: model.id, signal });
|
|
341
342
|
}
|
|
342
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Build the {@link ApiKeyResolver} handed to `streamSimple` for a gateway
|
|
346
|
+
* request. Drives the central a/b/c auth-retry policy server-side:
|
|
347
|
+
*
|
|
348
|
+
* - initial resolve → the credential already resolved for this request.
|
|
349
|
+
* - step (b) `!lastChance` → force-refresh the SAME session-sticky credential
|
|
350
|
+
* (a peer/broker may have rotated its token out from under our cached copy).
|
|
351
|
+
* - step (c) `lastChance` → {@link refreshGatewayApiKeyAfterAuthError} switches
|
|
352
|
+
* to a sibling (usage-limit block vs credential invalidation by error class).
|
|
353
|
+
*
|
|
354
|
+
* `lastKey` tracks the most recent bearer so the switch step invalidates the
|
|
355
|
+
* credential that actually failed.
|
|
356
|
+
*/
|
|
357
|
+
function buildGatewayApiKeyResolver(
|
|
358
|
+
storage: AuthStorage,
|
|
359
|
+
model: Model<Api>,
|
|
360
|
+
sessionId: string,
|
|
361
|
+
initialKey: string,
|
|
362
|
+
requestSignal: AbortSignal,
|
|
363
|
+
format: string,
|
|
364
|
+
peer: string,
|
|
365
|
+
): ApiKeyResolver {
|
|
366
|
+
let lastKey = initialKey;
|
|
367
|
+
return async ({ lastChance, error, signal }) => {
|
|
368
|
+
const sig = signal ?? requestSignal;
|
|
369
|
+
if (error === undefined) {
|
|
370
|
+
lastKey = initialKey;
|
|
371
|
+
return initialKey;
|
|
372
|
+
}
|
|
373
|
+
if (!lastChance) {
|
|
374
|
+
const refreshed = await storage.getApiKey(model.provider, sessionId, {
|
|
375
|
+
modelId: model.id,
|
|
376
|
+
signal: sig,
|
|
377
|
+
forceRefresh: true,
|
|
378
|
+
});
|
|
379
|
+
lastKey = refreshed ?? lastKey;
|
|
380
|
+
return refreshed;
|
|
381
|
+
}
|
|
382
|
+
const next = await refreshGatewayApiKeyAfterAuthError(
|
|
383
|
+
storage,
|
|
384
|
+
model,
|
|
385
|
+
sessionId,
|
|
386
|
+
model.provider,
|
|
387
|
+
lastKey,
|
|
388
|
+
error,
|
|
389
|
+
sig,
|
|
390
|
+
format,
|
|
391
|
+
peer,
|
|
392
|
+
);
|
|
393
|
+
lastKey = next ?? lastKey;
|
|
394
|
+
return next;
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
343
398
|
function clientClosedResponse(route: { module: FormatModule }): Response {
|
|
344
399
|
return route.module.formatError(499, "request_aborted", "client closed request");
|
|
345
400
|
}
|
|
@@ -447,19 +502,15 @@ async function handleFormatEndpoint(
|
|
|
447
502
|
}
|
|
448
503
|
|
|
449
504
|
const streamOpts = buildStreamOptions(parsed, model.api, controller.signal);
|
|
450
|
-
streamOpts.apiKey =
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
controller.signal,
|
|
460
|
-
route.label,
|
|
461
|
-
peer,
|
|
462
|
-
);
|
|
505
|
+
streamOpts.apiKey = buildGatewayApiKeyResolver(
|
|
506
|
+
bootOpts.storage,
|
|
507
|
+
model,
|
|
508
|
+
sessionId,
|
|
509
|
+
apiKey,
|
|
510
|
+
controller.signal,
|
|
511
|
+
route.label,
|
|
512
|
+
peer,
|
|
513
|
+
);
|
|
463
514
|
|
|
464
515
|
logger.info("auth-gateway request", {
|
|
465
516
|
format: route.label,
|
|
@@ -604,18 +655,15 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
|
|
604
655
|
// only inject server-controlled fields. The codex temperature/topP strip
|
|
605
656
|
// matches `buildStreamOptions` — Codex rejects them with a 400.
|
|
606
657
|
const streamOpts: SimpleStreamOptions = { ...parsed.options, apiKey, signal: controller.signal };
|
|
607
|
-
streamOpts.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
"pi-native",
|
|
617
|
-
peer,
|
|
618
|
-
);
|
|
658
|
+
streamOpts.apiKey = buildGatewayApiKeyResolver(
|
|
659
|
+
bootOpts.storage,
|
|
660
|
+
model,
|
|
661
|
+
sessionId,
|
|
662
|
+
apiKey,
|
|
663
|
+
controller.signal,
|
|
664
|
+
"pi-native",
|
|
665
|
+
peer,
|
|
666
|
+
);
|
|
619
667
|
if (model.api === "openai-codex-responses") {
|
|
620
668
|
delete streamOpts.temperature;
|
|
621
669
|
delete streamOpts.topP;
|