@oh-my-pi/pi-ai 18.1.21 → 18.2.0

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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.2.0] - 2026-09-15
6
+
7
+ ### Added
8
+
9
+ - Assistant turns from Anthropic-compatible hosts (direct, or via OpenRouter's `reasoning_details`) carry `upstreamModel`, the serving model id recovered from the signed thinking block, so callers can detect a router substituting a different model than requested.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed OpenCode Go window-limit 429s (`5-hour`/`Weekly`/`Monthly usage limit reached. Resets in …`) not pinning the exhausted credential to the server-stated reset; the window phrasing is now covered by a regression test over the rotation classifier. ([#12091](https://github.com/can1357/oh-my-pi/pull/12091) by [@H4vC](https://github.com/H4vC))
14
+
15
+ ## [18.1.22] - 2026-09-14
16
+
17
+ ### Fixed
18
+
19
+ - 400-request debug dumps now redact provider-specific auth headers (`x-goog-api-key`, `x-amz-security-token`, and any header whose name carries a key/token/secret), not just a fixed allow-list, so a shared dump can no longer leak a live API key ([#12007](https://github.com/can1357/oh-my-pi/issues/12007)).
20
+
5
21
  ## [18.1.20] - 2026-09-13
6
22
 
7
23
  ### Fixed
@@ -10843,7 +10843,7 @@ RUST RUNTIME DEPENDENCY LICENSES
10843
10843
 
10844
10844
  Generated from Cargo.lock by cargo-about 0.8.2 in locked, offline, workspace, all-feature, all-target mode, then restricted using cargo metadata to normal and build edges reachable from the workspace (development-only edges are excluded). cargo-deny independently evaluates the complete all-target graph.
10845
10845
 
10846
- Native Opus (audiopus/audiopus_sys) and PCRE2 (pcre2/pcre2-sys) are covered by the exact crate license payloads below. inferno 0.12.8 is package-scoped as CDDL-1.0: its CDDL-covered source remains available in the locked crates.io source archive at https://crates.io/crates/inferno/0.12.8 and https://github.com/jonhoo/inferno. The full locked license text follows below.
10846
+ Native Opus (opus/opusic-sys) and PCRE2 (pcre2/pcre2-sys) are covered by the exact crate license payloads below. inferno 0.12.8 is package-scoped as CDDL-1.0: its CDDL-covered source remains available in the locked crates.io source archive at https://crates.io/crates/inferno/0.12.8 and https://github.com/jonhoo/inferno. The full locked license text follows below.
10847
10847
 
10848
10848
  -------------------------------------------------------------------------------
10849
10849
  License: Apache License 2.0 (Apache-2.0)
@@ -12509,6 +12509,58 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
12509
12509
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
12510
12510
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12511
12511
 
12512
+ -------------------------------------------------------------------------------
12513
+ License: BSD 3-Clause "New" or "Revised" License (BSD-3-Clause)
12514
+ Used by:
12515
+ - opusic-sys 0.7.5 (https://github.com/DoumanAsh/opusic-sys)
12516
+
12517
+ Full license text:
12518
+
12519
+ Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic,
12520
+ Jean-Marc Valin, Timothy B. Terriberry,
12521
+ CSIRO, Gregory Maxwell, Mark Borgerding,
12522
+ Erik de Castro Lopo, Mozilla, Amazon
12523
+
12524
+ Redistribution and use in source and binary forms, with or without
12525
+ modification, are permitted provided that the following conditions
12526
+ are met:
12527
+
12528
+ - Redistributions of source code must retain the above copyright
12529
+ notice, this list of conditions and the following disclaimer.
12530
+
12531
+ - Redistributions in binary form must reproduce the above copyright
12532
+ notice, this list of conditions and the following disclaimer in the
12533
+ documentation and/or other materials provided with the distribution.
12534
+
12535
+ - Neither the name of Internet Society, IETF or IETF Trust, nor the
12536
+ names of specific contributors, may be used to endorse or promote
12537
+ products derived from this software without specific prior written
12538
+ permission.
12539
+
12540
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
12541
+ ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
12542
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
12543
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
12544
+ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
12545
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
12546
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
12547
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
12548
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
12549
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
12550
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12551
+
12552
+ Opus is subject to the royalty-free patent licenses which are
12553
+ specified at:
12554
+
12555
+ Xiph.Org Foundation:
12556
+ https://datatracker.ietf.org/ipr/1524/
12557
+
12558
+ Microsoft Corporation:
12559
+ https://datatracker.ietf.org/ipr/1914/
12560
+
12561
+ Broadcom Corporation:
12562
+ https://datatracker.ietf.org/ipr/1526/
12563
+
12512
12564
  -------------------------------------------------------------------------------
12513
12565
  License: BSD 3-Clause "New" or "Revised" License (BSD-3-Clause)
12514
12566
  Used by:
@@ -13290,29 +13342,6 @@ jurisdiction of the Federal Courts of the Northern District of
13290
13342
  California and the state courts of the State of California, with
13291
13343
  venue lying in Santa Clara County, California.
13292
13344
 
13293
- -------------------------------------------------------------------------------
13294
- License: ISC License (ISC)
13295
- Used by:
13296
- - audiopus_sys 0.2.2 (https://github.com/lakelezz/audiopus_sys.git)
13297
-
13298
- Full license text:
13299
-
13300
- ISC License
13301
-
13302
- Copyright (c) 2019, Lakelezz
13303
-
13304
- Permission to use, copy, modify, and/or distribute this software for any
13305
- purpose with or without fee is hereby granted, provided that the above
13306
- copyright notice and this permission notice appear in all copies.
13307
-
13308
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13309
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13310
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13311
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13312
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13313
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13314
- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
13315
-
13316
13345
  -------------------------------------------------------------------------------
13317
13346
  License: ISC License (ISC)
13318
13347
  Used by:
@@ -18734,7 +18763,7 @@ DEALINGS IN THE SOFTWARE.
18734
18763
  -------------------------------------------------------------------------------
18735
18764
  License: MIT License (MIT)
18736
18765
  Used by:
18737
- - opus 0.3.1 (https://github.com/SpaceManiac/opus-rs)
18766
+ - opus 0.4.0 (https://github.com/SpaceManiac/opus-rs)
18738
18767
 
18739
18768
  Full license text:
18740
18769
 
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Recovers the serving model id Anthropic embeds in thinking-block signatures.
3
+ *
4
+ * A signature is a base64 protobuf: outer `{1: version, 2: envelope}`, envelope
5
+ * `{1: header, 2..5: nonce/ciphertext/tag}`, header
6
+ * `{1: schema, 5: hash, 6: model, 8: "thinking", 11: org uuid, 15: profile, 21: unix ts}`.
7
+ * Field 6 names the model that actually produced the block — a concrete
8
+ * snapshot (`claude-haiku-4-5-20251001`) or an internal codename
9
+ * (`numbat-v6-…`) — which lets a caller notice a gateway serving something other
10
+ * than the requested model. The v4 outer format (`CAQS…`) ships a header
11
+ * without field 6; the decoder then yields nothing.
12
+ */
13
+ /**
14
+ * Model id embedded in an Anthropic thinking signature, or undefined when the
15
+ * signature is malformed, uses a format without a cleartext header, or names
16
+ * nothing resembling a model id.
17
+ */
18
+ export declare function servedModelFromAnthropicSignature(signature: string): string | undefined;
19
+ /**
20
+ * Model id from an OpenRouter reasoning item — a `reasoning_details` entry on
21
+ * chat completions or a Responses `reasoning` output item — which forwards the
22
+ * upstream Anthropic signature as `{ format: "anthropic-claude-v1", signature }`.
23
+ * Undefined for other formats.
24
+ */
25
+ export declare function servedModelFromOpenRouterReasoning(item: unknown): string | undefined;
@@ -60,6 +60,8 @@ export interface ProviderDefinition {
60
60
  readonly envKeys?: KeyResolver;
61
61
  /** Provider transport can authenticate without a resolved API-key string. */
62
62
  readonly allowsMissingApiKey?: boolean;
63
+ /** APIs whose provider transport resolves credentials without a stored account. */
64
+ readonly nativeAuthApis?: readonly string[];
63
65
  /** Provider-owned model normalization that must run before API-specific option mapping. */
64
66
  readonly prepareModel?: ProviderModelPreparer;
65
67
  /** Provider-owned request shaping applied before generic API dispatch. */
@@ -839,7 +839,13 @@ export interface AssistantMessage {
839
839
  * providers that expose no such field.
840
840
  */
841
841
  upstreamProvider?: string;
842
- /** Provider-reported concrete model when a router selected one for this turn. */
842
+ /**
843
+ * Concrete model that produced this turn when it is knowable independently
844
+ * of the requested id: reported by a router that selected one, or recovered
845
+ * from a signed thinking block (Anthropic signatures name the serving
846
+ * model). Compared against `model` to notice a gateway serving something
847
+ * other than what was requested.
848
+ */
843
849
  upstreamModel?: string;
844
850
  usage: Usage;
845
851
  stopReason: StopReason;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.1.21",
3
+ "version": "18.2.0",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -124,11 +124,11 @@
124
124
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
125
125
  },
126
126
  "dependencies": {
127
- "@oh-my-pi/omptype": "18.1.21",
128
- "@oh-my-pi/pi-catalog": "18.1.21",
129
- "@oh-my-pi/pi-natives": "18.1.21",
130
- "@oh-my-pi/pi-utils": "18.1.21",
131
- "@oh-my-pi/pi-wire": "18.1.21"
127
+ "@oh-my-pi/omptype": "18.2.0",
128
+ "@oh-my-pi/pi-catalog": "18.2.0",
129
+ "@oh-my-pi/pi-natives": "18.2.0",
130
+ "@oh-my-pi/pi-utils": "18.2.0",
131
+ "@oh-my-pi/pi-wire": "18.2.0"
132
132
  },
133
133
  "devDependencies": {
134
134
  "@types/bun": "^1.3.14"
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Recovers the serving model id Anthropic embeds in thinking-block signatures.
3
+ *
4
+ * A signature is a base64 protobuf: outer `{1: version, 2: envelope}`, envelope
5
+ * `{1: header, 2..5: nonce/ciphertext/tag}`, header
6
+ * `{1: schema, 5: hash, 6: model, 8: "thinking", 11: org uuid, 15: profile, 21: unix ts}`.
7
+ * Field 6 names the model that actually produced the block — a concrete
8
+ * snapshot (`claude-haiku-4-5-20251001`) or an internal codename
9
+ * (`numbat-v6-…`) — which lets a caller notice a gateway serving something other
10
+ * than the requested model. The v4 outer format (`CAQS…`) ships a header
11
+ * without field 6; the decoder then yields nothing.
12
+ */
13
+
14
+ const OUTER_ENVELOPE_FIELD = 2;
15
+ const ENVELOPE_HEADER_FIELD = 1;
16
+ const HEADER_MODEL_FIELD = 6;
17
+ const MAX_MODEL_ID_LENGTH = 128;
18
+ const MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
19
+
20
+ /** Returns the first length-delimited field `field` in `message`, or undefined. */
21
+ function lengthDelimitedField(message: Uint8Array, field: number): Uint8Array | undefined {
22
+ let offset = 0;
23
+ while (offset < message.length) {
24
+ let tag = 0;
25
+ let shift = 0;
26
+ let byte: number;
27
+ do {
28
+ if (offset >= message.length) return undefined;
29
+ byte = message[offset++];
30
+ tag |= (byte & 0x7f) << shift;
31
+ shift += 7;
32
+ } while (byte & 0x80);
33
+ const fieldNumber = tag >>> 3;
34
+ const wireType = tag & 7;
35
+ switch (wireType) {
36
+ case 0: {
37
+ do {
38
+ if (offset >= message.length) return undefined;
39
+ byte = message[offset++];
40
+ } while (byte & 0x80);
41
+ break;
42
+ }
43
+ case 1:
44
+ offset += 8;
45
+ break;
46
+ case 2: {
47
+ let length = 0;
48
+ shift = 0;
49
+ do {
50
+ if (offset >= message.length) return undefined;
51
+ byte = message[offset++];
52
+ length |= (byte & 0x7f) << shift;
53
+ shift += 7;
54
+ } while (byte & 0x80);
55
+ if (offset + length > message.length) return undefined;
56
+ if (fieldNumber === field) return message.subarray(offset, offset + length);
57
+ offset += length;
58
+ break;
59
+ }
60
+ case 5:
61
+ offset += 4;
62
+ break;
63
+ default:
64
+ return undefined;
65
+ }
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ /**
71
+ * Model id embedded in an Anthropic thinking signature, or undefined when the
72
+ * signature is malformed, uses a format without a cleartext header, or names
73
+ * nothing resembling a model id.
74
+ */
75
+ export function servedModelFromAnthropicSignature(signature: string): string | undefined {
76
+ if (signature.length < 8 || signature.length > 8192) return undefined;
77
+ let raw: Uint8Array;
78
+ try {
79
+ raw = Buffer.from(signature, "base64");
80
+ } catch {
81
+ return undefined;
82
+ }
83
+ const envelope = lengthDelimitedField(raw, OUTER_ENVELOPE_FIELD);
84
+ if (!envelope) return undefined;
85
+ const header = lengthDelimitedField(envelope, ENVELOPE_HEADER_FIELD);
86
+ if (!header) return undefined;
87
+ const modelBytes = lengthDelimitedField(header, HEADER_MODEL_FIELD);
88
+ if (!modelBytes || modelBytes.length === 0 || modelBytes.length > MAX_MODEL_ID_LENGTH) return undefined;
89
+ const model = Buffer.from(modelBytes).toString("utf8");
90
+ return MODEL_ID_PATTERN.test(model) ? model : undefined;
91
+ }
92
+
93
+ /**
94
+ * Model id from an OpenRouter reasoning item — a `reasoning_details` entry on
95
+ * chat completions or a Responses `reasoning` output item — which forwards the
96
+ * upstream Anthropic signature as `{ format: "anthropic-claude-v1", signature }`.
97
+ * Undefined for other formats.
98
+ */
99
+ export function servedModelFromOpenRouterReasoning(item: unknown): string | undefined {
100
+ if (!item || typeof item !== "object" || !("format" in item) || !("signature" in item)) return undefined;
101
+ if (item.format !== "anthropic-claude-v1" || typeof item.signature !== "string") return undefined;
102
+ return servedModelFromAnthropicSignature(item.signature);
103
+ }
@@ -122,6 +122,7 @@ import {
122
122
  resolveGitHubCopilotBaseUrl,
123
123
  wrapFetchForCopilotFallback,
124
124
  } from "./github-copilot-headers";
125
+ import { servedModelFromAnthropicSignature } from "./anthropic-signature";
125
126
  import { getOpenAIPromptCacheKey } from "./openai-shared";
126
127
  import { applyInferenceHeaders } from "./inference-headers";
127
128
  import { redactSensitiveCredentials, transformMessages } from "./transform-messages";
@@ -2588,6 +2589,11 @@ const streamAnthropicOnce = (
2588
2589
  if (unwrappedThinking !== undefined) {
2589
2590
  block.thinking = unwrappedThinking;
2590
2591
  block.thinkingSignature = undefined;
2592
+ } else if (!output.upstreamModel && block.thinkingSignature) {
2593
+ // The signature names the model that actually produced the
2594
+ // block; a gateway serving a different model than requested
2595
+ // cannot mint one that says otherwise.
2596
+ output.upstreamModel = servedModelFromAnthropicSignature(block.thinkingSignature);
2591
2597
  }
2592
2598
  stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: output });
2593
2599
  } else if (block.type === "anthropicServerTool" && block.block.type === "server_tool_use") {
@@ -3289,6 +3295,7 @@ const streamAnthropicOnce = (
3289
3295
  output.content.length = 0;
3290
3296
  output.model = model.id;
3291
3297
  output.responseId = undefined;
3298
+ output.upstreamModel = undefined;
3292
3299
  output.errorMessage = undefined;
3293
3300
  output.providerPayload = undefined;
3294
3301
  output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
@@ -3318,6 +3325,7 @@ const streamAnthropicOnce = (
3318
3325
  output.content.length = 0;
3319
3326
  output.model = model.id;
3320
3327
  output.responseId = undefined;
3328
+ output.upstreamModel = undefined;
3321
3329
  output.errorMessage = undefined;
3322
3330
  output.inputTransformations = undefined;
3323
3331
  output.providerPayload = undefined;
@@ -3351,6 +3359,7 @@ const streamAnthropicOnce = (
3351
3359
  output.content.length = 0;
3352
3360
  output.model = model.id;
3353
3361
  output.responseId = undefined;
3362
+ output.upstreamModel = undefined;
3354
3363
  output.errorMessage = undefined;
3355
3364
  output.providerPayload = undefined;
3356
3365
  output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
@@ -3378,6 +3387,7 @@ const streamAnthropicOnce = (
3378
3387
  output.content.length = 0;
3379
3388
  output.model = model.id;
3380
3389
  output.responseId = undefined;
3390
+ output.upstreamModel = undefined;
3381
3391
  output.errorMessage = undefined;
3382
3392
  output.providerPayload = undefined;
3383
3393
  output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
@@ -78,6 +78,7 @@ import {
78
78
  rememberOpenAIReasoningEffortFallback,
79
79
  resolveOpenAIReasoningEffortFallback,
80
80
  } from "./openai-reasoning-fallback";
81
+ import { servedModelFromOpenRouterReasoning } from "./anthropic-signature";
81
82
  import { resolveCopilotRequestIdentity, wrapFetchForCopilotFallback } from "./github-copilot-headers";
82
83
  import {
83
84
  applyChatCompletionsReasoningParams,
@@ -1379,6 +1380,7 @@ const streamOpenAICompletionsOnce = (
1379
1380
  if (Array.isArray(reasoningDetails)) {
1380
1381
  for (const detail of reasoningDetails) {
1381
1382
  if (!detail || typeof detail !== "object") continue;
1383
+ if (!output.upstreamModel) output.upstreamModel = servedModelFromOpenRouterReasoning(detail);
1382
1384
  const detailObject = detail as { type?: unknown; id?: unknown; data?: unknown };
1383
1385
  if (detailObject.type === "reasoning.encrypted" && detailObject.id && detailObject.data) {
1384
1386
  const matchingToolCall = output.content.find(
@@ -97,6 +97,7 @@ import {
97
97
  hasCopilotVisionInput,
98
98
  resolveGitHubCopilotBaseUrl,
99
99
  } from "./github-copilot-headers";
100
+ import { servedModelFromOpenRouterReasoning } from "./anthropic-signature";
100
101
  import type { ChatCompletionCreateParamsStreaming } from "./openai-chat-wire";
101
102
  import type { InputItem } from "./openai-codex/request-transformer";
102
103
  import type {
@@ -3250,6 +3251,7 @@ export async function processResponsesStream<TApi extends Api>(
3250
3251
  if (reasoningBlock) {
3251
3252
  reasoningBlock.thinking = finalizeReasoningThinking(item, reasoningBlock.thinking);
3252
3253
  reasoningBlock.thinkingSignature = JSON.stringify(item);
3254
+ if (!output.upstreamModel) output.upstreamModel = servedModelFromOpenRouterReasoning(item);
3253
3255
  stream.push({
3254
3256
  type: "thinking_end",
3255
3257
  contentIndex: contentIndexOf(reasoningBlock),
@@ -62,6 +62,7 @@ export function buildProviderDefinition(
62
62
  ...transport,
63
63
  ...(envKeys !== undefined ? { envKeys } : {}),
64
64
  ...(policy.allowsMissingApiKey !== undefined ? { allowsMissingApiKey: policy.allowsMissingApiKey } : {}),
65
+ ...(policy.nativeAuthApis !== undefined ? { nativeAuthApis: policy.nativeAuthApis } : {}),
65
66
  ...(policy.available !== undefined ? { available: policy.available } : {}),
66
67
  ...(policy.showInLoginList !== undefined ? { showInLoginList: policy.showInLoginList } : {}),
67
68
  ...(policy.storeAs !== undefined ? { storeCredentialsAs: policy.storeAs } : {}),
@@ -69,6 +69,8 @@ export interface ProviderDefinition {
69
69
  readonly envKeys?: KeyResolver;
70
70
  /** Provider transport can authenticate without a resolved API-key string. */
71
71
  readonly allowsMissingApiKey?: boolean;
72
+ /** APIs whose provider transport resolves credentials without a stored account. */
73
+ readonly nativeAuthApis?: readonly string[];
72
74
  /** Provider-owned model normalization that must run before API-specific option mapping. */
73
75
  readonly prepareModel?: ProviderModelPreparer;
74
76
  /** Provider-owned request shaping applied before generic API dispatch. */
package/src/stream.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  requireSupportedEffort,
14
14
  resolveWireModelId,
15
15
  } from "@oh-my-pi/pi-catalog/model-thinking";
16
- import { CATALOG_PROVIDERS, type ProviderCatalogEntry } from "@oh-my-pi/pi-catalog/provider-models";
16
+ import { providerEntries } from "@oh-my-pi/pi-catalog/compat/providers";
17
17
  import { CODEX_BASE_URL } from "@oh-my-pi/pi-catalog/wire/codex";
18
18
  import { $env, $pickenv, getProviderInFlightRoot, isEnoent, logger } from "@oh-my-pi/pi-utils";
19
19
  import { getCustomApi } from "./api-registry";
@@ -815,11 +815,12 @@ const LEGACY_ENV_KEYS: Record<string, KeyResolver> = {
815
815
  };
816
816
 
817
817
  /**
818
- * Env fallbacks derived from the catalog table — the single source for plain
819
- * provider env-var names. Registry defs override with computed resolvers
820
- * (Foundry/ADC/Bedrock probes); legacy non-provider keys merge last.
818
+ * Env fallbacks derived from the catalog provider entries (`env` in
819
+ * `providers/<id>.kdl`) — the single source for plain provider env-var names.
820
+ * Registry defs override with computed resolvers (Foundry/ADC/Bedrock
821
+ * probes); legacy non-provider keys merge last.
821
822
  */
822
- const CATALOG_ENTRY_ENV_KEYS = (CATALOG_PROVIDERS as readonly ProviderCatalogEntry[]).flatMap(provider => {
823
+ const CATALOG_ENTRY_ENV_KEYS = Object.values(providerEntries()).flatMap(provider => {
823
824
  const envVars = provider.envVars;
824
825
  if (!envVars || envVars.length === 0) return [];
825
826
  const resolver: KeyResolver = envVars.length === 1 ? envVars[0] : () => $pickenv(...envVars);
package/src/types.ts CHANGED
@@ -1041,7 +1041,13 @@ export interface AssistantMessage {
1041
1041
  * providers that expose no such field.
1042
1042
  */
1043
1043
  upstreamProvider?: string;
1044
- /** Provider-reported concrete model when a router selected one for this turn. */
1044
+ /**
1045
+ * Concrete model that produced this turn when it is knowable independently
1046
+ * of the requested id: reported by a router that selected one, or recovered
1047
+ * from a signed thinking block (Anthropic signatures name the serving
1048
+ * model). Compared against `model` to notice a gateway serving something
1049
+ * other than what was requested.
1050
+ */
1045
1051
  upstreamModel?: string;
1046
1052
  usage: Usage;
1047
1053
  stopReason: StopReason;
@@ -20,7 +20,16 @@ export type CapturedHttpErrorResponse = {
20
20
  bodyJson?: unknown;
21
21
  };
22
22
 
23
- const SENSITIVE_HEADERS = ["authorization", "x-api-key", "api-key", "cookie", "set-cookie", "proxy-authorization"];
23
+ /**
24
+ * Matches any header name whose value carries a credential, so a persisted dump
25
+ * never leaks one. A substring match — not a hand-maintained allow-list — so
26
+ * provider-specific auth headers (`x-goog-api-key`, `x-amz-security-token`, …)
27
+ * are redacted without enumerating every provider's spelling; the fixed list it
28
+ * replaced silently leaked any auth header it did not name. Redacting a benign
29
+ * header that happens to match is harmless: dumps exist to diagnose the request
30
+ * body, not its transport headers.
31
+ */
32
+ const SENSITIVE_HEADER_PATTERN = /key|token|secret|auth|credential|cookie/i;
24
33
 
25
34
  /**
26
35
  * Build the JSON persisted for a rejected request. Request fields stay at the
@@ -167,7 +176,7 @@ function redactHeaders(headers: Record<string, string> | undefined): Record<stri
167
176
 
168
177
  const redacted: Record<string, string> = {};
169
178
  for (const [key, value] of Object.entries(headers)) {
170
- if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
179
+ if (SENSITIVE_HEADER_PATTERN.test(key)) {
171
180
  redacted[key] = "[redacted]";
172
181
  continue;
173
182
  }