@oh-my-pi/pi-ai 18.2.4 → 18.2.6
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 +17 -0
- package/dist/types/registry/engine/common.d.ts +1 -1
- package/package.json +6 -6
- package/src/auth-broker/discover.ts +5 -3
- package/src/providers/anthropic.ts +131 -26
- package/src/registry/engine/common.ts +2 -1
- package/src/registry/engine/oauth-code.ts +10 -2
- package/src/registry/engine/refresh.ts +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.2.6] - 2026-09-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Anthropic prompt-cache head re-baselining on every memory recall refresh: the system breakpoint now anchors on the last stable segment instead of the volatile recall suffix, and the stable-system fingerprint ignores recall blocks, so a recall refresh re-bills only the suffix instead of the whole tools+system head.
|
|
10
|
+
- Fixed auth-broker client config resolution failing silently on Windows when reading the token file or `config.yml`; reads now use `node:fs` instead of `Bun.file`.
|
|
11
|
+
|
|
12
|
+
## [18.2.5] - 2026-09-17
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Added support for templating and custom base and authentication URLs in OAuth flows.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fixed Anthropic prompt-cache breakpoints stalling when conversations include mid-conversation tool changes, preventing growing message tails from being unnecessarily re-billed as uncached input.
|
|
21
|
+
|
|
5
22
|
## [18.2.4] - 2026-09-17
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -48,7 +48,7 @@ export declare function postTokenRequest(request: CompiledOAuthRequest, standard
|
|
|
48
48
|
response: Response;
|
|
49
49
|
}>;
|
|
50
50
|
/** Bearer GET declared by `userinfo`; failures leave identity fields unset. */
|
|
51
|
-
export declare function applyUserinfo(userinfo: CompiledUserinfo | undefined, credentials: OAuthCredentials, context: RequestContext): Promise<OAuthCredentials>;
|
|
51
|
+
export declare function applyUserinfo(userinfo: CompiledUserinfo | undefined, credentials: OAuthCredentials, context: RequestContext, vars?: TemplateVars): Promise<OAuthCredentials>;
|
|
52
52
|
/** Runs the rule's after-exchange / after-refresh hook, if declared. */
|
|
53
53
|
export declare function applyAfterExchange(hook: string | undefined, credentials: OAuthCredentials, context: ExchangeContext): Promise<OAuthCredentials>;
|
|
54
54
|
export declare function describeError(error: unknown): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.2.
|
|
3
|
+
"version": "18.2.6",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -128,11 +128,11 @@
|
|
|
128
128
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
129
129
|
},
|
|
130
130
|
"dependencies": {
|
|
131
|
-
"@oh-my-pi/omptype": "18.2.
|
|
132
|
-
"@oh-my-pi/pi-catalog": "18.2.
|
|
133
|
-
"@oh-my-pi/pi-natives": "18.2.
|
|
134
|
-
"@oh-my-pi/pi-utils": "18.2.
|
|
135
|
-
"@oh-my-pi/pi-wire": "18.2.
|
|
131
|
+
"@oh-my-pi/omptype": "18.2.6",
|
|
132
|
+
"@oh-my-pi/pi-catalog": "18.2.6",
|
|
133
|
+
"@oh-my-pi/pi-natives": "18.2.6",
|
|
134
|
+
"@oh-my-pi/pi-utils": "18.2.6",
|
|
135
|
+
"@oh-my-pi/pi-wire": "18.2.6"
|
|
136
136
|
},
|
|
137
137
|
"devDependencies": {
|
|
138
138
|
"@types/bun": "^1.3.14"
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* token file → local SQLite) in one place so build-time tooling sees the same
|
|
5
5
|
* credentials as the TUI.
|
|
6
6
|
*/
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
7
8
|
import * as path from "node:path";
|
|
8
9
|
import {
|
|
9
10
|
$envExact,
|
|
@@ -60,7 +61,7 @@ async function defaultResolveConfigValue(config: string): Promise<string | undef
|
|
|
60
61
|
|
|
61
62
|
async function readTokenFile(): Promise<string | null> {
|
|
62
63
|
try {
|
|
63
|
-
const raw = await
|
|
64
|
+
const raw = await fs.readFile(getAuthBrokerTokenFilePath(), "utf8");
|
|
64
65
|
const trimmed = raw.trim();
|
|
65
66
|
return trimmed.length > 0 ? trimmed : null;
|
|
66
67
|
} catch (err) {
|
|
@@ -99,7 +100,7 @@ async function readConfigYaml(agentDir: string): Promise<ConfigSnapshot> {
|
|
|
99
100
|
for (const filename of MAIN_CONFIG_FILENAMES) {
|
|
100
101
|
const configPath = path.join(agentDir, filename);
|
|
101
102
|
try {
|
|
102
|
-
const raw = await
|
|
103
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
103
104
|
const parsed = YAML.parse(raw);
|
|
104
105
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
105
106
|
const record = parsed as Record<string, unknown>;
|
|
@@ -121,7 +122,8 @@ export async function loadAuthBrokerAccountPool(): Promise<AuthBrokerAccountPool
|
|
|
121
122
|
|
|
122
123
|
let parsed: unknown;
|
|
123
124
|
try {
|
|
124
|
-
|
|
125
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
126
|
+
parsed = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
|
|
125
127
|
} catch (error) {
|
|
126
128
|
throw new AIError.ConfigurationError(`Unable to read OMP_AUTH_BROKER_ACCOUNT_POOL_FILE at ${filePath}`, {
|
|
127
129
|
cause: error,
|
|
@@ -1563,7 +1563,9 @@ async function* iterateAnthropicEvents(
|
|
|
1563
1563
|
let sawMessageStart = false;
|
|
1564
1564
|
let sawMessageEnd = false;
|
|
1565
1565
|
|
|
1566
|
-
|
|
1566
|
+
// Capture `raw` only when the diagnostic observer exists; otherwise the
|
|
1567
|
+
// per-frame wire-line array is pure token-path garbage.
|
|
1568
|
+
for await (const sse of readSseEvents(response.body, signal, onSseEvent ? { captureRaw: true } : undefined)) {
|
|
1567
1569
|
notifyRawSseEvent(onSseEvent, sse);
|
|
1568
1570
|
if (sse.event === "error") {
|
|
1569
1571
|
throw createAnthropicSseStreamError(sse.data);
|
|
@@ -3952,8 +3954,17 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
3952
3954
|
const messageEnd = hasTrailingAssistantPad ? trailingIndex - 1 : trailingIndex;
|
|
3953
3955
|
|
|
3954
3956
|
// A breakpoint caches every preceding byte, not only the decorated message.
|
|
3955
|
-
//
|
|
3956
|
-
//
|
|
3957
|
+
// A per-call or turn-scoped message is rebuilt next request, so a prefix
|
|
3958
|
+
// spanning it cannot match — but only at its own position. Messages after
|
|
3959
|
+
// the mark are ordinary persisted history with stable bytes, so a later
|
|
3960
|
+
// breakpoint still matches everything after the mark. The cost is bounded
|
|
3961
|
+
// to re-billing the marked bytes themselves, not the growing tail.
|
|
3962
|
+
// Hence two anchors, not a truncation: the newest candidate at or before
|
|
3963
|
+
// the first per-call/turn-scoped message (when one exists) pins the
|
|
3964
|
+
// reusable prefix behind the mark, and the rolling tail candidates pin
|
|
3965
|
+
// the suffix after it. Turn-scoped `clear_at` messages are absent next
|
|
3966
|
+
// request, so they still truncate the decimation range (ordinals would
|
|
3967
|
+
// shift), but per-call marks no longer freeze the tail.
|
|
3957
3968
|
let stableMessageEnd = messageEnd;
|
|
3958
3969
|
for (let index = 0; index <= messageEnd; index++) {
|
|
3959
3970
|
const message = params.messages[index];
|
|
@@ -3978,18 +3989,39 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
3978
3989
|
// Stable historical decimation checkpoint every 15 user turns (15th, 30th, 45th...)
|
|
3979
3990
|
const decimationIndices = userIndices.filter((_, ordinal) => (ordinal + 1) % ANTHROPIC_DECIMATION_INTERVAL === 0);
|
|
3980
3991
|
|
|
3981
|
-
// Collect up to 2 trailing candidates from the
|
|
3992
|
+
// Collect up to 2 trailing candidates from the message tail, skipping
|
|
3993
|
+
// per-call messages, turn-scoped messages, and mid-conversation
|
|
3994
|
+
// tool-control messages. A per-call tail candidate is rebuilt next request
|
|
3995
|
+
// (fresh timestamps on appended probes, fresh redaction bytes), so a
|
|
3996
|
+
// breakpoint on it cannot match — it would spend the tail anchor on bytes
|
|
3997
|
+
// that never repeat while the persisted history behind it goes uncached.
|
|
3998
|
+
// Turn-scoped messages are absent next request for the same reason, and
|
|
3999
|
+
// tool controls reject cache_control outright. The walk starts at the
|
|
4000
|
+
// message tail (not the truncated prefix end) so the anchor advances every
|
|
4001
|
+
// turn; the sub-prefix candidate below covers the reusable region behind
|
|
4002
|
+
// a mark.
|
|
3982
4003
|
const trailingCandidates: number[] = [];
|
|
3983
|
-
for (let index =
|
|
4004
|
+
for (let index = messageEnd; index >= 0 && trailingCandidates.length < 2; index--) {
|
|
3984
4005
|
const message = params.messages[index];
|
|
3985
|
-
if (!message) continue;
|
|
4006
|
+
if (!message || message.clear_at === "next_user_message" || isPerCallContextMessage(message)) continue;
|
|
4007
|
+
if (
|
|
4008
|
+
message.role === "system" &&
|
|
4009
|
+
typeof message.content !== "string" &&
|
|
4010
|
+
Array.isArray(message.content) &&
|
|
4011
|
+
message.content.length > 0 &&
|
|
4012
|
+
message.content.every(block => block.type === "tool_addition" || block.type === "tool_removal")
|
|
4013
|
+
) {
|
|
4014
|
+
continue;
|
|
4015
|
+
}
|
|
3986
4016
|
trailingCandidates.push(index);
|
|
3987
4017
|
}
|
|
3988
|
-
|
|
3989
4018
|
// Prioritize:
|
|
3990
4019
|
// 1. Most recent trailing message
|
|
3991
4020
|
// 2. Latest decimation checkpoints (newest first) to maintain stable long-context anchors
|
|
3992
|
-
// 3.
|
|
4021
|
+
// 3. Newest message at or before the first per-call/turn-scoped mark, so a
|
|
4022
|
+
// volatile interior message costs only its own re-billed bytes instead
|
|
4023
|
+
// of invalidating the whole reusable prefix behind it
|
|
4024
|
+
// 4. Second trailing message
|
|
3993
4025
|
const candidateIndices: number[] = [];
|
|
3994
4026
|
if (trailingCandidates.length > 0) {
|
|
3995
4027
|
candidateIndices.push(trailingCandidates[0]);
|
|
@@ -3999,6 +4031,9 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
3999
4031
|
candidateIndices.push(decimationIndices[i]);
|
|
4000
4032
|
}
|
|
4001
4033
|
}
|
|
4034
|
+
if (stableMessageEnd < messageEnd && stableMessageEnd >= 0 && !candidateIndices.includes(stableMessageEnd)) {
|
|
4035
|
+
candidateIndices.push(stableMessageEnd);
|
|
4036
|
+
}
|
|
4002
4037
|
for (const index of trailingCandidates) {
|
|
4003
4038
|
if (!candidateIndices.includes(index)) {
|
|
4004
4039
|
candidateIndices.push(index);
|
|
@@ -4017,17 +4052,57 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
4017
4052
|
}
|
|
4018
4053
|
}
|
|
4019
4054
|
|
|
4055
|
+
/**
|
|
4056
|
+
* Trailing system-prompt segments carrying per-turn volatile content (memory
|
|
4057
|
+
* recall blocks). They are rendered by the coding agent as their own
|
|
4058
|
+
* `systemPrompt` array elements and appended last, so on the wire they
|
|
4059
|
+
* normally form a volatile suffix after the stable prefix. The system cache
|
|
4060
|
+
* breakpoint anchors on the last stable segment instead of the array tail, so
|
|
4061
|
+
* a recall refresh re-bills only the suffix and the message tail for one turn
|
|
4062
|
+
* while the tools+stable-system prefix stays a cache hit. The fingerprint in
|
|
4063
|
+
* `planStableAnthropicSystem` is scoped the same way, so a recall-only change
|
|
4064
|
+
* no longer resets the tool/control baselines either.
|
|
4065
|
+
*
|
|
4066
|
+
* Only a genuinely trailing volatile run counts: a `before_agent_start`
|
|
4067
|
+
* extension override may append a stable policy block after the staged recall
|
|
4068
|
+
* block, and that block must stay fingerprinted stable (a change to it has to
|
|
4069
|
+
* re-baseline). A volatile block stranded mid-array still poisons the prefix
|
|
4070
|
+
* at its position — prefix caching is positional, so no classification can
|
|
4071
|
+
* save the bytes after it — but the stable tail is at least fingerprinted
|
|
4072
|
+
* instead of silently excluded.
|
|
4073
|
+
*
|
|
4074
|
+
* Detection is by our own markup, not model identity: recall blocks always
|
|
4075
|
+
* open with `<memories>`. Stable segments containing recalled text elsewhere
|
|
4076
|
+
* (e.g. quoted in conversation) are unaffected — only a leading tag counts.
|
|
4077
|
+
*/
|
|
4078
|
+
const VOLATILE_SYSTEM_SEGMENT_MARKERS = ["<memories>"];
|
|
4079
|
+
|
|
4080
|
+
function stableSystemSuffixStart(systemBlocks: readonly AnthropicSystemBlock[]): number {
|
|
4081
|
+
let start = systemBlocks.length;
|
|
4082
|
+
while (start > 0) {
|
|
4083
|
+
const text = systemBlocks[start - 1]?.text ?? "";
|
|
4084
|
+
if (!VOLATILE_SYSTEM_SEGMENT_MARKERS.some(marker => text.startsWith(marker))) break;
|
|
4085
|
+
start--;
|
|
4086
|
+
}
|
|
4087
|
+
return start;
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4020
4090
|
/**
|
|
4021
4091
|
* Anchor cache_control on the stable request head — the last (non-deferred)
|
|
4022
|
-
* tool definition and the last system block. The canonical cache order is
|
|
4023
|
-
* tools → system → messages, so a breakpoint on the final system block caches
|
|
4024
|
-
* the entire tools+system prefix, and the extra tool breakpoint keeps the tool
|
|
4092
|
+
* tool definition and the last stable system block. The canonical cache order is
|
|
4093
|
+
* tools → system → messages, so a breakpoint on the final stable system block caches
|
|
4094
|
+
* the entire tools+stable-system prefix, and the extra tool breakpoint keeps the tool
|
|
4025
4095
|
* definitions cached even when the system text changes. This guarantees the
|
|
4026
4096
|
* large, unchanging head is a cache hit on every turn regardless of how the
|
|
4027
4097
|
* message tail churns — the breakpoint placement first-party Anthropic clients
|
|
4028
4098
|
* (Claude Code, Pi) use. Without it, the general API-key path anchors only the
|
|
4029
4099
|
* moving message tail, so tail churn re-writes the whole head uncached.
|
|
4030
4100
|
*
|
|
4101
|
+
* Volatile trailing segments (memory recall) sit after the breakpoint, so a
|
|
4102
|
+
* recall refresh re-bills only the suffix and the tail for one turn instead of
|
|
4103
|
+
* the whole head. When every system block is volatile there is no stable
|
|
4104
|
+
* boundary and the breakpoint stays on the array tail (previous behavior).
|
|
4105
|
+
*
|
|
4031
4106
|
* Anthropic allows at most 4 cache breakpoints per request. At most one is
|
|
4032
4107
|
* spent on tools and one on system here, leaving the remaining budget for
|
|
4033
4108
|
* the message tail and historical decimation checkpoints in `applyPromptCaching`.
|
|
@@ -4063,9 +4138,28 @@ function applyHeadCaching(
|
|
|
4063
4138
|
}
|
|
4064
4139
|
}
|
|
4065
4140
|
|
|
4066
|
-
if (systemBlocks && systemBlocks.length > 0
|
|
4067
|
-
|
|
4068
|
-
|
|
4141
|
+
if (systemBlocks && systemBlocks.length > 0) {
|
|
4142
|
+
// Anchor on the last stable block so a volatile recall suffix refresh
|
|
4143
|
+
// re-bills only the suffix, not the whole head. The skip-if-decorated
|
|
4144
|
+
// check applies only when there is no volatile suffix (previous
|
|
4145
|
+
// behavior): with a suffix present the boundary anchor is added
|
|
4146
|
+
// whenever the anchor block itself lacks a breakpoint, even if the
|
|
4147
|
+
// OAuth path pre-decorated its identity block — otherwise the only
|
|
4148
|
+
// system breakpoint sits before the stable prompt and a recall
|
|
4149
|
+
// refresh re-bills it. The message budget in `applyPromptCaching`
|
|
4150
|
+
// shrinks accordingly (4 minus head breakpoints). All-volatile falls
|
|
4151
|
+
// back to tail anchoring (previous behavior).
|
|
4152
|
+
const suffixStart = stableSystemSuffixStart(systemBlocks);
|
|
4153
|
+
if (suffixStart === systemBlocks.length) {
|
|
4154
|
+
if (!systemBlocks.some(block => block.cache_control != null)) {
|
|
4155
|
+
const lastBlock = systemBlocks[systemBlocks.length - 1];
|
|
4156
|
+
if (lastBlock) lastBlock.cache_control = cloneAnthropicCacheControl(cacheControl);
|
|
4157
|
+
}
|
|
4158
|
+
} else {
|
|
4159
|
+
const anchorIndex = suffixStart === 0 ? systemBlocks.length - 1 : suffixStart - 1;
|
|
4160
|
+
const anchor = systemBlocks[anchorIndex];
|
|
4161
|
+
if (anchor && anchor.cache_control == null) anchor.cache_control = cloneAnthropicCacheControl(cacheControl);
|
|
4162
|
+
}
|
|
4069
4163
|
}
|
|
4070
4164
|
}
|
|
4071
4165
|
|
|
@@ -4149,11 +4243,15 @@ function getAnthropicControlState(
|
|
|
4149
4243
|
): AnthropicControlState | undefined {
|
|
4150
4244
|
if (!state) return undefined;
|
|
4151
4245
|
const root = messages[0];
|
|
4246
|
+
// Key on the stable system prefix, not the full array: a volatile recall
|
|
4247
|
+
// suffix refresh must resolve the same baseline or the declared-tool,
|
|
4248
|
+
// effort, and control-transition state it preserves is lost with it.
|
|
4249
|
+
const stablePrefix = system?.slice(0, stableSystemSuffixStart(system)) ?? null;
|
|
4152
4250
|
const fingerprint = String(
|
|
4153
4251
|
Bun.hash(
|
|
4154
4252
|
JSON.stringify([
|
|
4155
4253
|
sessionId ?? "",
|
|
4156
|
-
|
|
4254
|
+
stablePrefix?.map(block => block.text) ?? null,
|
|
4157
4255
|
root ? anthropicControlMessageProjection(root) : null,
|
|
4158
4256
|
]),
|
|
4159
4257
|
),
|
|
@@ -4199,14 +4297,16 @@ function syncAnthropicControlState(state: AnthropicControlState, messages: reado
|
|
|
4199
4297
|
}
|
|
4200
4298
|
|
|
4201
4299
|
/**
|
|
4202
|
-
* Keep the top-level `system` array byte-stable across a session. The
|
|
4203
|
-
* captured on the first request
|
|
4204
|
-
* request's cache breakpoints) while
|
|
4300
|
+
* Keep the top-level `system` array byte-stable across a session. The stable
|
|
4301
|
+
* prefix captured on the first request is replayed verbatim (with the current
|
|
4302
|
+
* request's cache breakpoints) while its text is unchanged; the volatile
|
|
4303
|
+
* recall suffix always passes through current-turn. A stable-prefix change
|
|
4205
4304
|
* re-baselines instead of duplicating the prompt as a mid-conversation
|
|
4206
4305
|
* system message: omp's system prompt is one rendered segment that embeds
|
|
4207
4306
|
* the tool roster, so replaying a second copy on every later request would
|
|
4208
4307
|
* cost the full prompt again per change. The prefix rewrite is absorbed by
|
|
4209
|
-
* `prefix_mismatch_behavior: "drop_block"` and one cache miss
|
|
4308
|
+
* `prefix_mismatch_behavior: "drop_block"` and one cache miss, while a
|
|
4309
|
+
* recall-only change keeps the tool/control baselines intact.
|
|
4210
4310
|
*/
|
|
4211
4311
|
function planStableAnthropicSystem(
|
|
4212
4312
|
current: AnthropicSystemBlock[] | undefined,
|
|
@@ -4214,16 +4314,21 @@ function planStableAnthropicSystem(
|
|
|
4214
4314
|
enabled: boolean,
|
|
4215
4315
|
): AnthropicSystemBlock[] | undefined {
|
|
4216
4316
|
if (!state || !enabled) return current;
|
|
4217
|
-
const
|
|
4317
|
+
const suffixStart = stableSystemSuffixStart(current ?? []);
|
|
4318
|
+
const fingerprint = JSON.stringify(current?.slice(0, suffixStart).map(block => block.text) ?? null);
|
|
4218
4319
|
if (state.systemFingerprint !== fingerprint) {
|
|
4219
4320
|
resetAnthropicControlState(state);
|
|
4220
4321
|
state.systemFingerprint = fingerprint;
|
|
4221
|
-
state.stableSystemBlocks = current?.map(block => ({ type: block.type, text: block.text }));
|
|
4222
|
-
}
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4322
|
+
state.stableSystemBlocks = current?.slice(0, suffixStart).map(block => ({ type: block.type, text: block.text }));
|
|
4323
|
+
}
|
|
4324
|
+
const stableReplay =
|
|
4325
|
+
state.stableSystemBlocks?.map((block, index) => {
|
|
4326
|
+
const cacheControl = current?.[index]?.cache_control;
|
|
4327
|
+
return cacheControl ? { ...block, cache_control: cloneAnthropicCacheControl(cacheControl) } : { ...block };
|
|
4328
|
+
}) ?? [];
|
|
4329
|
+
const suffix = current?.slice(suffixStart).map(block => ({ ...block })) ?? [];
|
|
4330
|
+
const replayed = [...stableReplay, ...suffix];
|
|
4331
|
+
return replayed.length > 0 ? replayed : undefined;
|
|
4227
4332
|
}
|
|
4228
4333
|
|
|
4229
4334
|
function anthropicToolDefinitionKey(tool: AnthropicWireTool): string {
|
|
@@ -267,10 +267,11 @@ export async function applyUserinfo(
|
|
|
267
267
|
userinfo: CompiledUserinfo | undefined,
|
|
268
268
|
credentials: OAuthCredentials,
|
|
269
269
|
context: RequestContext,
|
|
270
|
+
vars: TemplateVars = {},
|
|
270
271
|
): Promise<OAuthCredentials> {
|
|
271
272
|
if (!userinfo) return credentials;
|
|
272
273
|
try {
|
|
273
|
-
const response = await context.fetch(userinfo.url, {
|
|
274
|
+
const response = await context.fetch(template(userinfo.url, vars), {
|
|
274
275
|
headers: { ...context.headers, Authorization: `Bearer ${credentials.access}` },
|
|
275
276
|
signal: context.signal,
|
|
276
277
|
});
|
|
@@ -93,6 +93,8 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
|
|
|
93
93
|
#verifier = "";
|
|
94
94
|
#clientId?: string;
|
|
95
95
|
#clientSecret?: string;
|
|
96
|
+
#base?: string;
|
|
97
|
+
#auth?: string;
|
|
96
98
|
|
|
97
99
|
constructor(
|
|
98
100
|
ctrl: OAuthController,
|
|
@@ -123,6 +125,8 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
|
|
|
123
125
|
const signal = this.ctrl.signal;
|
|
124
126
|
this.#clientId = rule.clientId ? await resolveValue(rule.clientId, signal) : undefined;
|
|
125
127
|
this.#clientSecret = rule.clientSecret ? await resolveValue(rule.clientSecret, signal) : undefined;
|
|
128
|
+
this.#base = rule.baseUrl ? (await resolveValue(rule.baseUrl, signal)).replace(/\/+$/, "") : undefined;
|
|
129
|
+
this.#auth = rule.authUrl ? (await resolveValue(rule.authUrl, signal)).replace(/\/+$/, "") : undefined;
|
|
126
130
|
let challenge: string | undefined;
|
|
127
131
|
if (rule.pkce) {
|
|
128
132
|
const pkce = await generatePKCE();
|
|
@@ -136,6 +140,8 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
|
|
|
136
140
|
scope,
|
|
137
141
|
state,
|
|
138
142
|
code_challenge: challenge,
|
|
143
|
+
base: this.#base,
|
|
144
|
+
auth: this.#auth,
|
|
139
145
|
};
|
|
140
146
|
const params = new URLSearchParams();
|
|
141
147
|
if (rule.standardAuthorizeParams) {
|
|
@@ -150,7 +156,7 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
|
|
|
150
156
|
if (state) params.set("state", state);
|
|
151
157
|
}
|
|
152
158
|
for (const key in rule.authorizeParams) params.set(key, template(rule.authorizeParams[key], vars));
|
|
153
|
-
const authorizeUrl = await resolveValue(rule.authorizeUrl, signal);
|
|
159
|
+
const authorizeUrl = template(await resolveValue(rule.authorizeUrl, signal), vars);
|
|
154
160
|
return { url: `${authorizeUrl}?${params.toString()}`, instructions: rule.instructions };
|
|
155
161
|
}
|
|
156
162
|
|
|
@@ -185,6 +191,8 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
|
|
|
185
191
|
code_verifier: rule.pkce ? this.#verifier : undefined,
|
|
186
192
|
client_id: this.#clientId,
|
|
187
193
|
client_secret: this.#clientSecret,
|
|
194
|
+
base: this.#base,
|
|
195
|
+
auth: this.#auth,
|
|
188
196
|
};
|
|
189
197
|
const context = { provider: this.#provider, fetch: this.#fetch, signal };
|
|
190
198
|
const { body } = await postTokenRequest(
|
|
@@ -203,7 +211,7 @@ export class DeclarativeOAuthCodeFlow extends OAuthCallbackFlow {
|
|
|
203
211
|
);
|
|
204
212
|
throwIfCancelled(signal);
|
|
205
213
|
let credentials = mapCredentials(rule.credential, body, this.#provider);
|
|
206
|
-
credentials = await applyUserinfo(rule.userinfo, credentials, context);
|
|
214
|
+
credentials = await applyUserinfo(rule.userinfo, credentials, context, vars);
|
|
207
215
|
throwIfCancelled(signal);
|
|
208
216
|
return applyAfterExchange(rule.afterExchange, credentials, {
|
|
209
217
|
provider: this.#provider,
|
|
@@ -31,7 +31,8 @@ async function loginClient(policy: CompiledAuthProvider, signal?: AbortSignal) {
|
|
|
31
31
|
redirectUri: login.callback.redirectUri
|
|
32
32
|
? await resolveValue(login.callback.redirectUri, signal)
|
|
33
33
|
: `http://${login.callback.hostname}:${login.callback.port}${login.callback.path}`,
|
|
34
|
-
base: undefined,
|
|
34
|
+
base: login.baseUrl ? (await resolveValue(login.baseUrl, signal)).replace(/\/+$/, "") : undefined,
|
|
35
|
+
auth: login.authUrl ? (await resolveValue(login.authUrl, signal)).replace(/\/+$/, "") : undefined,
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
if (login?.kind === "device-code") {
|
|
@@ -40,9 +41,10 @@ async function loginClient(policy: CompiledAuthProvider, signal?: AbortSignal) {
|
|
|
40
41
|
clientSecret: undefined,
|
|
41
42
|
redirectUri: undefined,
|
|
42
43
|
base: login.baseUrl ? await resolveValue(login.baseUrl, signal) : undefined,
|
|
44
|
+
auth: undefined,
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
|
-
return { clientId: undefined, clientSecret: undefined, redirectUri: undefined, base: undefined };
|
|
47
|
+
return { clientId: undefined, clientSecret: undefined, redirectUri: undefined, base: undefined, auth: undefined };
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
function createRequestRefresh(rule: RequestRefresh, policy: CompiledAuthProvider): Refresher {
|
|
@@ -66,6 +68,7 @@ function createRequestRefresh(rule: RequestRefresh, policy: CompiledAuthProvider
|
|
|
66
68
|
client_secret: client.clientSecret,
|
|
67
69
|
redirect_uri: client.redirectUri,
|
|
68
70
|
base: client.base,
|
|
71
|
+
auth: client.auth,
|
|
69
72
|
claude_code_sdk_version: claudeCodeSdkVersion,
|
|
70
73
|
};
|
|
71
74
|
const { body } = await postTokenRequest(
|