oira666_pi-limits-wait 0.4.13 → 0.4.15
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/package.json +1 -1
- package/src/constants.ts +7 -0
- package/src/index.ts +30 -6
- package/src/models.ts +3 -1
- package/src/prompt.ts +111 -4
- package/src/stream.ts +27 -9
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -10,6 +10,13 @@ export const PI_REMOVAL_ANCHORS = [
|
|
|
10
10
|
export const PI_IDENTITY_SENTENCE_PATTERN =
|
|
11
11
|
/(?:^|\n)\s*You are pi\b[^.!?\n]*(?:[.!?](?=\s|$)|(?=\n|$))/gi;
|
|
12
12
|
|
|
13
|
+
// Strips the host-harness identity clause pi prepends to its system prompt, e.g.
|
|
14
|
+
// "You are an expert coding assistant operating inside pi, a coding agent harness."
|
|
15
|
+
// The Claude Code identity block already establishes who the assistant is, so
|
|
16
|
+
// leaving this clause in tells the model (and Anthropic) it is running inside pi.
|
|
17
|
+
export const PI_HARNESS_IDENTITY_PATTERN =
|
|
18
|
+
/\s+operating inside pi,\s*a coding agent harness/gi;
|
|
19
|
+
|
|
13
20
|
export const CLAUDE_CODE_IDENTITY_PATTERN =
|
|
14
21
|
/(?:^|\n)\s*You are Claude Code, Anthropic's official CLI for Claude\.\s*/gi;
|
|
15
22
|
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { isInternalSyntheticModel } from "./models.js";
|
|
3
|
+
import { sanitiseAnthropicPayloadSystem } from "./prompt.js";
|
|
3
4
|
import { loadFallbackSettings } from "./settings.js";
|
|
4
5
|
import { state } from "./state.js";
|
|
5
6
|
import { registerWrappedApi } from "./stream.js";
|
|
6
7
|
|
|
7
|
-
export { sanitiseSystemPrompt } from "./prompt.js";
|
|
8
|
+
export { sanitiseAnthropicPayloadSystem, sanitiseSystemPrompt } from "./prompt.js";
|
|
8
9
|
export {
|
|
9
10
|
getRetryableError,
|
|
10
11
|
isAuthenticationRefreshError,
|
|
@@ -29,9 +30,26 @@ export {
|
|
|
29
30
|
export { freezingEnabled, waitForRateLimit, waitForRetry } from "./ui.js";
|
|
30
31
|
export type { FallbackModel } from "./types.js";
|
|
31
32
|
|
|
33
|
+
function registerContextApis(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
34
|
+
for (const model of ctx.modelRegistry.getAll()) {
|
|
35
|
+
if (!isInternalSyntheticModel(model)) registerWrappedApi(pi, model.api);
|
|
36
|
+
}
|
|
37
|
+
if (ctx.model && !isInternalSyntheticModel(ctx.model)) registerWrappedApi(pi, ctx.model.api);
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
export default function (pi: ExtensionAPI) {
|
|
33
41
|
state.extensionApi = pi;
|
|
34
42
|
|
|
43
|
+
// Anthropic now rejects Claude Pro/Max (OAuth) requests whose system prompt
|
|
44
|
+
// still carries the host agent's fingerprints (pi-coding-agent paths, "You
|
|
45
|
+
// are pi", etc.). pi-ai prepends the Claude Code identity block for OAuth
|
|
46
|
+
// tokens but passes the host system prompt through as a second block, which
|
|
47
|
+
// trips the check. Sanitise that block on the outbound payload right before
|
|
48
|
+
// it is sent. This hook fires for every provider request and only mutates
|
|
49
|
+
// Anthropic OAuth payloads (identified by the Claude Code identity block), so
|
|
50
|
+
// API-key Anthropic calls and non-Anthropic providers are untouched.
|
|
51
|
+
pi.on("before_provider_request", (event) => sanitiseAnthropicPayloadSystem(event.payload));
|
|
52
|
+
|
|
35
53
|
pi.on("model_select", (event) => {
|
|
36
54
|
if (state.suppressNextModelSelect) {
|
|
37
55
|
state.suppressNextModelSelect = false;
|
|
@@ -50,14 +68,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
50
68
|
|
|
51
69
|
// Some extensions may register providers after this extension loads. Wrap
|
|
52
70
|
// any APIs that exist by the time an agent starts too.
|
|
53
|
-
|
|
54
|
-
if (!isInternalSyntheticModel(model)) registerWrappedApi(pi, model.api);
|
|
55
|
-
}
|
|
56
|
-
if (ctx.model && !isInternalSyntheticModel(ctx.model)) registerWrappedApi(pi, ctx.model.api);
|
|
71
|
+
registerContextApis(pi, ctx);
|
|
57
72
|
|
|
58
73
|
// Anthropic subscription/OAuth identity is applied per provider request in
|
|
59
74
|
// streamWithLimitsRetry(). Doing it there ensures every retry/fallback
|
|
60
75
|
// attempt gets the right prompt for its actual target model, while
|
|
61
76
|
// non-Anthropic providers never inherit the Claude Code identity.
|
|
62
77
|
});
|
|
78
|
+
|
|
79
|
+
// Run once more after all before_agent_start handlers have completed and the
|
|
80
|
+
// agent loop is about to make its first provider request. This catches
|
|
81
|
+
// providers that another extension registered during before_agent_start after
|
|
82
|
+
// our handler had already run.
|
|
83
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
84
|
+
state.sharedCtx = ctx;
|
|
85
|
+
registerContextApis(pi, ctx);
|
|
86
|
+
});
|
|
63
87
|
}
|
package/src/models.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
1
2
|
import type { Api, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
|
|
2
3
|
import { DEFAULT_NON_RETRYABLE_FREEZE_MS } from "./constants.js";
|
|
3
4
|
import { isRateLimitError, parseRetryDelayMs } from "./retry-errors.js";
|
|
@@ -147,13 +148,14 @@ export async function optionsForModel(
|
|
|
147
148
|
originalModel: Model<Api>,
|
|
148
149
|
target: FallbackModel,
|
|
149
150
|
options?: SimpleStreamOptions,
|
|
151
|
+
ctx: ExtensionContext | undefined = state.sharedCtx,
|
|
150
152
|
): Promise<SimpleStreamOptions | undefined> {
|
|
151
153
|
const level = target.reasoningEffort ?? state.primaryThinkingLevel;
|
|
152
154
|
const reasoning = level && level !== "off" ? level : undefined;
|
|
153
155
|
if (modelKey(originalModel) === modelKey(target.model)) {
|
|
154
156
|
return reasoning ? { ...options, reasoning } : options;
|
|
155
157
|
}
|
|
156
|
-
const auth = await
|
|
158
|
+
const auth = await ctx?.modelRegistry.getApiKeyAndHeaders(target.model);
|
|
157
159
|
if (!auth?.ok) throw new Error(auth ? auth.error : "Model registry is unavailable.");
|
|
158
160
|
return {
|
|
159
161
|
...options,
|
package/src/prompt.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { Api, Context, Model } from "@mariozechner/pi-ai";
|
|
3
|
-
import { CLAUDE_CODE_IDENTITY, CLAUDE_CODE_IDENTITY_PATTERN, PI_IDENTITY_SENTENCE_PATTERN, PI_REMOVAL_ANCHORS } from "./constants.js";
|
|
2
|
+
import type { Api, Context, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
|
|
3
|
+
import { CLAUDE_CODE_IDENTITY, CLAUDE_CODE_IDENTITY_PATTERN, PI_HARNESS_IDENTITY_PATTERN, PI_IDENTITY_SENTENCE_PATTERN, PI_REMOVAL_ANCHORS } from "./constants.js";
|
|
4
4
|
import { state } from "./state.js";
|
|
5
5
|
|
|
6
6
|
export function sanitiseSystemPrompt(raw: string): string {
|
|
@@ -13,6 +13,7 @@ export function sanitiseSystemPrompt(raw: string): string {
|
|
|
13
13
|
.join("\n\n")
|
|
14
14
|
.replace(CLAUDE_CODE_IDENTITY_PATTERN, "")
|
|
15
15
|
.replace(PI_IDENTITY_SENTENCE_PATTERN, "")
|
|
16
|
+
.replace(PI_HARNESS_IDENTITY_PATTERN, "")
|
|
16
17
|
.replace(/\n{3,}/g, "\n\n")
|
|
17
18
|
.trim();
|
|
18
19
|
}
|
|
@@ -28,8 +29,56 @@ export function isAnthropicOAuthSession(ctx: ExtensionContext): boolean {
|
|
|
28
29
|
);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
export function
|
|
32
|
-
|
|
32
|
+
export function isAnthropicOAuthToken(value: string | undefined): boolean {
|
|
33
|
+
return Boolean(value?.includes("sk-ant-oat"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function headerValue(headers: Record<string, string> | undefined, name: string): string | undefined {
|
|
37
|
+
if (!headers) return undefined;
|
|
38
|
+
const direct = headers[name];
|
|
39
|
+
if (direct !== undefined) return direct;
|
|
40
|
+
const lower = name.toLowerCase();
|
|
41
|
+
return Object.entries(headers).find(([key]) => key.toLowerCase() === lower)?.[1];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function requestAuthIsAnthropicOAuth(options?: SimpleStreamOptions): boolean | undefined {
|
|
45
|
+
if (typeof options?.apiKey === "string" && options.apiKey.length > 0) {
|
|
46
|
+
return isAnthropicOAuthToken(options.apiKey);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const authorization = headerValue(options?.headers, "authorization");
|
|
50
|
+
if (authorization) {
|
|
51
|
+
const bearer = authorization.match(/^\s*Bearer\s+(.+?)\s*$/i)?.[1] ?? authorization;
|
|
52
|
+
return isAnthropicOAuthToken(bearer);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isAnthropicSubscriptionRequest(
|
|
59
|
+
model: Model<Api>,
|
|
60
|
+
options?: SimpleStreamOptions,
|
|
61
|
+
ctx: ExtensionContext | undefined = state.sharedCtx,
|
|
62
|
+
): boolean {
|
|
63
|
+
if (model.provider !== "anthropic") return false;
|
|
64
|
+
|
|
65
|
+
// Prefer the actual per-request auth. This avoids both false negatives
|
|
66
|
+
// (OAuth token supplied via options/fallback auth while modelRegistry does not
|
|
67
|
+
// report OAuth) and false positives (registry has OAuth, but this request was
|
|
68
|
+
// explicitly made with a normal API key).
|
|
69
|
+
const requestAuth = requestAuthIsAnthropicOAuth(options);
|
|
70
|
+
if (requestAuth !== undefined) return requestAuth;
|
|
71
|
+
|
|
72
|
+
return Boolean(ctx?.modelRegistry.isUsingOAuth(model));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function anthropicSubscriptionContext(
|
|
76
|
+
model: Model<Api>,
|
|
77
|
+
context: Context,
|
|
78
|
+
options?: SimpleStreamOptions,
|
|
79
|
+
ctx: ExtensionContext | undefined = state.sharedCtx,
|
|
80
|
+
): Context {
|
|
81
|
+
if (!isAnthropicSubscriptionRequest(model, options, ctx)) return context;
|
|
33
82
|
|
|
34
83
|
const sanitised = sanitiseSystemPrompt(context.systemPrompt ?? "");
|
|
35
84
|
return {
|
|
@@ -39,3 +88,61 @@ export function anthropicSubscriptionContext(model: Model<Api>, context: Context
|
|
|
39
88
|
: CLAUDE_CODE_IDENTITY,
|
|
40
89
|
};
|
|
41
90
|
}
|
|
91
|
+
|
|
92
|
+
interface SystemTextBlock {
|
|
93
|
+
type: string;
|
|
94
|
+
text?: unknown;
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isClaudeCodeIdentityBlock(block: unknown): block is SystemTextBlock {
|
|
99
|
+
if (!block || typeof block !== "object") return false;
|
|
100
|
+
const b = block as SystemTextBlock;
|
|
101
|
+
return b.type === "text" && typeof b.text === "string" && b.text.includes(CLAUDE_CODE_IDENTITY);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sanitise the `system` blocks of an Anthropic provider request payload.
|
|
106
|
+
*
|
|
107
|
+
* Anthropic now rejects Claude Pro/Max (OAuth) requests whose system prompt
|
|
108
|
+
* still carries third-party-agent fingerprints (e.g. pi-coding-agent paths or
|
|
109
|
+
* "You are pi" identity). pi-ai already prepends the Claude Code identity as
|
|
110
|
+
* the first `system` block for OAuth tokens, but passes the host agent's full
|
|
111
|
+
* system prompt through as a second block unchanged, which trips the check.
|
|
112
|
+
*
|
|
113
|
+
* This runs from the `before_provider_request` extension hook, which fires for
|
|
114
|
+
* every provider request and whose return value replaces the outbound payload.
|
|
115
|
+
* It only touches payloads that already carry the Claude Code identity block
|
|
116
|
+
* (i.e. Anthropic OAuth requests), so API-key Anthropic calls and non-Anthropic
|
|
117
|
+
* providers are left untouched. Each remaining text block is run through
|
|
118
|
+
* `sanitiseSystemPrompt`; blocks that become empty are dropped.
|
|
119
|
+
*/
|
|
120
|
+
export function sanitiseAnthropicPayloadSystem(payload: unknown): unknown {
|
|
121
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
122
|
+
const system = (payload as { system?: unknown }).system;
|
|
123
|
+
if (!Array.isArray(system)) return payload;
|
|
124
|
+
|
|
125
|
+
const identityIndex = system.findIndex(isClaudeCodeIdentityBlock);
|
|
126
|
+
if (identityIndex === -1) return payload;
|
|
127
|
+
|
|
128
|
+
const newSystem: unknown[] = [];
|
|
129
|
+
for (let i = 0; i < system.length; i++) {
|
|
130
|
+
const block = system[i];
|
|
131
|
+
if (i === identityIndex) {
|
|
132
|
+
newSystem.push(block);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
block && typeof block === "object"
|
|
137
|
+
&& (block as SystemTextBlock).type === "text"
|
|
138
|
+
&& typeof (block as SystemTextBlock).text === "string"
|
|
139
|
+
) {
|
|
140
|
+
const sanitised = sanitiseSystemPrompt((block as SystemTextBlock).text as string);
|
|
141
|
+
if (sanitised) newSystem.push({ ...block, text: sanitised });
|
|
142
|
+
} else {
|
|
143
|
+
newSystem.push(block);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { ...payload, system: newSystem };
|
|
148
|
+
}
|
package/src/stream.ts
CHANGED
|
@@ -89,6 +89,7 @@ export function streamWithLimitsRetry(
|
|
|
89
89
|
const output = createAssistantMessageEventStream();
|
|
90
90
|
|
|
91
91
|
void (async () => {
|
|
92
|
+
const requestCtx = state.sharedCtx;
|
|
92
93
|
let committed = false;
|
|
93
94
|
const allowFallback = fallbackEnabled() && isFallbackEligibleModel(model);
|
|
94
95
|
let attempt: FallbackModel = allowFallback ? initialAttempt(model) : { model, reasoningEffort: state.primaryThinkingLevel };
|
|
@@ -124,14 +125,14 @@ export function streamWithLimitsRetry(
|
|
|
124
125
|
try {
|
|
125
126
|
try {
|
|
126
127
|
const attemptOptions = allowFallback
|
|
127
|
-
? await optionsForModel(model, attempt, options)
|
|
128
|
+
? await optionsForModel(model, attempt, options, requestCtx)
|
|
128
129
|
: options;
|
|
129
130
|
const attemptDelegate = attempt.model.api === model.api
|
|
130
131
|
? delegate
|
|
131
132
|
: state.builtinStreamSimpleByApi.get(attempt.model.api)
|
|
132
133
|
?? getApiProviders().find((provider) => provider.api === attempt.model.api)?.streamSimple;
|
|
133
134
|
if (!attemptDelegate) throw new Error(`No stream handler registered for API ${attempt.model.api}.`);
|
|
134
|
-
const attemptContext = anthropicSubscriptionContext(attempt.model, context);
|
|
135
|
+
const attemptContext = anthropicSubscriptionContext(attempt.model, context, attemptOptions, requestCtx);
|
|
135
136
|
const inner = await attemptDelegate(attempt.model, attemptContext, attemptOptions);
|
|
136
137
|
for await (const event of inner) {
|
|
137
138
|
if (!committed) {
|
|
@@ -338,17 +339,34 @@ export function streamWithLimitsRetry(
|
|
|
338
339
|
|
|
339
340
|
export const streamWithRateLimitRetry = streamWithLimitsRetry;
|
|
340
341
|
|
|
341
|
-
|
|
342
|
-
|
|
342
|
+
const LIMITS_WAIT_WRAPPER_FLAG = "__piLimitsWaitWrapper";
|
|
343
|
+
type LimitsWaitStreamSimpleFn = ((
|
|
344
|
+
model: Model<Api>,
|
|
345
|
+
context: Context,
|
|
346
|
+
options?: SimpleStreamOptions,
|
|
347
|
+
) => AssistantMessageEventStream) & { [LIMITS_WAIT_WRAPPER_FLAG]?: true };
|
|
343
348
|
|
|
344
|
-
|
|
345
|
-
|
|
349
|
+
export function registerWrappedApi(pi: ExtensionAPI, api: Api): void {
|
|
350
|
+
const currentStreamSimple = getApiProviders().find((provider) => provider.api === api)?.streamSimple;
|
|
351
|
+
if (!currentStreamSimple) return;
|
|
352
|
+
if ((currentStreamSimple as LimitsWaitStreamSimpleFn)[LIMITS_WAIT_WRAPPER_FLAG]) return;
|
|
346
353
|
|
|
347
|
-
state.builtinStreamSimpleByApi.set(api,
|
|
354
|
+
state.builtinStreamSimpleByApi.set(api, currentStreamSimple);
|
|
348
355
|
state.wrappedApis.add(api);
|
|
356
|
+
|
|
357
|
+
const wrappedStreamSimple = ((model: Model<Api>, context: Context, options?: SimpleStreamOptions) =>
|
|
358
|
+
streamWithLimitsRetry(currentStreamSimple, model, context, options)) as LimitsWaitStreamSimpleFn;
|
|
359
|
+
wrappedStreamSimple[LIMITS_WAIT_WRAPPER_FLAG] = true;
|
|
360
|
+
|
|
349
361
|
pi.registerProvider(`limits-wait-${api}`, {
|
|
350
362
|
api,
|
|
351
|
-
streamSimple:
|
|
352
|
-
streamWithLimitsRetry(builtinStreamSimple, model, context, options),
|
|
363
|
+
streamSimple: wrappedStreamSimple,
|
|
353
364
|
});
|
|
365
|
+
|
|
366
|
+
// pi-ai wraps provider functions when registering them, so the function later
|
|
367
|
+
// returned by getApiProviders() is not the same object as wrappedStreamSimple.
|
|
368
|
+
// Tag the registered wrapper too; otherwise repeated agent_start/model loops
|
|
369
|
+
// would wrap an already wrapped API again.
|
|
370
|
+
const registeredStreamSimple = getApiProviders().find((provider) => provider.api === api)?.streamSimple as LimitsWaitStreamSimpleFn | undefined;
|
|
371
|
+
if (registeredStreamSimple) registeredStreamSimple[LIMITS_WAIT_WRAPPER_FLAG] = true;
|
|
354
372
|
}
|