oira666_pi-limits-wait 0.4.13 → 0.4.14
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/index.ts +18 -5
- package/src/models.ts +3 -1
- package/src/prompt.ts +51 -3
- package/src/stream.ts +27 -9
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
3
|
import { loadFallbackSettings } from "./settings.js";
|
|
4
4
|
import { state } from "./state.js";
|
|
@@ -29,6 +29,13 @@ export {
|
|
|
29
29
|
export { freezingEnabled, waitForRateLimit, waitForRetry } from "./ui.js";
|
|
30
30
|
export type { FallbackModel } from "./types.js";
|
|
31
31
|
|
|
32
|
+
function registerContextApis(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
33
|
+
for (const model of ctx.modelRegistry.getAll()) {
|
|
34
|
+
if (!isInternalSyntheticModel(model)) registerWrappedApi(pi, model.api);
|
|
35
|
+
}
|
|
36
|
+
if (ctx.model && !isInternalSyntheticModel(ctx.model)) registerWrappedApi(pi, ctx.model.api);
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
export default function (pi: ExtensionAPI) {
|
|
33
40
|
state.extensionApi = pi;
|
|
34
41
|
|
|
@@ -50,14 +57,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
50
57
|
|
|
51
58
|
// Some extensions may register providers after this extension loads. Wrap
|
|
52
59
|
// 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);
|
|
60
|
+
registerContextApis(pi, ctx);
|
|
57
61
|
|
|
58
62
|
// Anthropic subscription/OAuth identity is applied per provider request in
|
|
59
63
|
// streamWithLimitsRetry(). Doing it there ensures every retry/fallback
|
|
60
64
|
// attempt gets the right prompt for its actual target model, while
|
|
61
65
|
// non-Anthropic providers never inherit the Claude Code identity.
|
|
62
66
|
});
|
|
67
|
+
|
|
68
|
+
// Run once more after all before_agent_start handlers have completed and the
|
|
69
|
+
// agent loop is about to make its first provider request. This catches
|
|
70
|
+
// providers that another extension registered during before_agent_start after
|
|
71
|
+
// our handler had already run.
|
|
72
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
73
|
+
state.sharedCtx = ctx;
|
|
74
|
+
registerContextApis(pi, ctx);
|
|
75
|
+
});
|
|
63
76
|
}
|
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,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { Api, Context, Model } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { Api, Context, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
|
|
3
3
|
import { CLAUDE_CODE_IDENTITY, CLAUDE_CODE_IDENTITY_PATTERN, PI_IDENTITY_SENTENCE_PATTERN, PI_REMOVAL_ANCHORS } from "./constants.js";
|
|
4
4
|
import { state } from "./state.js";
|
|
5
5
|
|
|
@@ -28,8 +28,56 @@ export function isAnthropicOAuthSession(ctx: ExtensionContext): boolean {
|
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function
|
|
32
|
-
|
|
31
|
+
export function isAnthropicOAuthToken(value: string | undefined): boolean {
|
|
32
|
+
return Boolean(value?.includes("sk-ant-oat"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function headerValue(headers: Record<string, string> | undefined, name: string): string | undefined {
|
|
36
|
+
if (!headers) return undefined;
|
|
37
|
+
const direct = headers[name];
|
|
38
|
+
if (direct !== undefined) return direct;
|
|
39
|
+
const lower = name.toLowerCase();
|
|
40
|
+
return Object.entries(headers).find(([key]) => key.toLowerCase() === lower)?.[1];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requestAuthIsAnthropicOAuth(options?: SimpleStreamOptions): boolean | undefined {
|
|
44
|
+
if (typeof options?.apiKey === "string" && options.apiKey.length > 0) {
|
|
45
|
+
return isAnthropicOAuthToken(options.apiKey);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const authorization = headerValue(options?.headers, "authorization");
|
|
49
|
+
if (authorization) {
|
|
50
|
+
const bearer = authorization.match(/^\s*Bearer\s+(.+?)\s*$/i)?.[1] ?? authorization;
|
|
51
|
+
return isAnthropicOAuthToken(bearer);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isAnthropicSubscriptionRequest(
|
|
58
|
+
model: Model<Api>,
|
|
59
|
+
options?: SimpleStreamOptions,
|
|
60
|
+
ctx: ExtensionContext | undefined = state.sharedCtx,
|
|
61
|
+
): boolean {
|
|
62
|
+
if (model.provider !== "anthropic") return false;
|
|
63
|
+
|
|
64
|
+
// Prefer the actual per-request auth. This avoids both false negatives
|
|
65
|
+
// (OAuth token supplied via options/fallback auth while modelRegistry does not
|
|
66
|
+
// report OAuth) and false positives (registry has OAuth, but this request was
|
|
67
|
+
// explicitly made with a normal API key).
|
|
68
|
+
const requestAuth = requestAuthIsAnthropicOAuth(options);
|
|
69
|
+
if (requestAuth !== undefined) return requestAuth;
|
|
70
|
+
|
|
71
|
+
return Boolean(ctx?.modelRegistry.isUsingOAuth(model));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function anthropicSubscriptionContext(
|
|
75
|
+
model: Model<Api>,
|
|
76
|
+
context: Context,
|
|
77
|
+
options?: SimpleStreamOptions,
|
|
78
|
+
ctx: ExtensionContext | undefined = state.sharedCtx,
|
|
79
|
+
): Context {
|
|
80
|
+
if (!isAnthropicSubscriptionRequest(model, options, ctx)) return context;
|
|
33
81
|
|
|
34
82
|
const sanitised = sanitiseSystemPrompt(context.systemPrompt ?? "");
|
|
35
83
|
return {
|
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
|
}
|