oira666_pi-limits-wait 0.4.11 → 0.4.13
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/README.md +87 -87
- package/package.json +1 -1
- package/src/constants.ts +26 -23
- package/src/fetch-observer.ts +49 -49
- package/src/index.ts +5 -12
- package/src/models.ts +180 -180
- package/src/prompt.ts +41 -26
- package/src/retry-errors.ts +263 -259
- package/src/settings.ts +130 -130
- package/src/state.ts +39 -39
- package/src/stream.ts +354 -352
- package/src/types.ts +41 -41
- package/src/ui.ts +142 -142
package/src/models.ts
CHANGED
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
import type { Api, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
|
|
2
|
-
import { DEFAULT_NON_RETRYABLE_FREEZE_MS } from "./constants.js";
|
|
3
|
-
import { isRateLimitError, parseRetryDelayMs } from "./retry-errors.js";
|
|
4
|
-
import { state } from "./state.js";
|
|
5
|
-
import type { FallbackModel, RetryableError } from "./types.js";
|
|
6
|
-
import { formatErrorDetail, formatDuration, reasonLabel } from "./ui.js";
|
|
7
|
-
|
|
8
|
-
export function modelKey(model: Pick<Model<Api>, "provider" | "id">): string {
|
|
9
|
-
return `${model.provider}/${model.id}`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function formatModel(model: Pick<Model<Api>, "provider" | "id">): string {
|
|
13
|
-
return `${model.provider}/${model.id}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function isInternalSyntheticModel(model: Pick<Model<Api>, "provider" | "id">): boolean {
|
|
17
|
-
return model.provider.startsWith("pi-") || model.id.startsWith("synthetic-");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function isFallbackEligibleModel(model: Model<Api>): boolean {
|
|
21
|
-
if (isInternalSyntheticModel(model)) return false;
|
|
22
|
-
const key = modelKey(model);
|
|
23
|
-
return state.primaryModel
|
|
24
|
-
? key === modelKey(state.primaryModel) || state.fallbackModels.some((entry) => modelKey(entry.model) === key)
|
|
25
|
-
: true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function fallbackEnabled(): boolean {
|
|
29
|
-
return state.fallbackModels.length > 0;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function getPrimaryModel(current: Model<Api>): Model<Api> {
|
|
33
|
-
return state.primaryModel ?? current;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function candidateOrder(current: Model<Api>): FallbackModel[] {
|
|
37
|
-
const primary = getPrimaryModel(current);
|
|
38
|
-
const order: FallbackModel[] = [{ model: primary, reasoningEffort: state.primaryThinkingLevel }];
|
|
39
|
-
const seen = new Set([modelKey(primary)]);
|
|
40
|
-
for (const entry of state.fallbackModels) {
|
|
41
|
-
const key = modelKey(entry.model);
|
|
42
|
-
if (seen.has(key)) continue;
|
|
43
|
-
seen.add(key);
|
|
44
|
-
order.push(entry);
|
|
45
|
-
}
|
|
46
|
-
return order;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function activeLimit(model: Model<Api>) {
|
|
50
|
-
const entry = state.rateLimitMemory.get(modelKey(model));
|
|
51
|
-
if (!entry) return undefined;
|
|
52
|
-
if (Date.now() >= entry.deadline) {
|
|
53
|
-
state.rateLimitMemory.delete(modelKey(model));
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
return entry;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function ensureRateLimitedModelsStatus(): void {
|
|
60
|
-
// Intentionally no-op: limited/frozen model information is communicated via
|
|
61
|
-
// chat notifications only. Do not render persistent TUI status lines.
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function notifyRetryableError(model: Model<Api>, retryable: RetryableError, errorMessage?: string): void {
|
|
65
|
-
const detail = errorMessage ? ` Error: ${formatErrorDetail(errorMessage, Number.MAX_SAFE_INTEGER)}` : "";
|
|
66
|
-
state.sharedCtx?.ui.notify(`${formatModel(model)} ${reasonLabel(retryable.reason).toLowerCase()} for ${formatDuration(retryable.waitMs)}.${detail}`, "warning");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function notifyRetryingAfterError(model: Model<Api>, waitMs: number, errorMessage: string): void {
|
|
70
|
-
state.sharedCtx?.ui.notify(`${formatModel(model)} retrying after error for ${formatDuration(waitMs)}. Error: ${formatErrorDetail(errorMessage, Number.MAX_SAFE_INTEGER)}`, "warning");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function rememberRateLimit(model: Model<Api>, retryable: RetryableError, errorMessage?: string): void {
|
|
74
|
-
const deadline = Date.now() + retryable.waitMs;
|
|
75
|
-
state.rateLimitMemory.set(modelKey(model), { reason: retryable.reason, limitedAt: Date.now(), deadline });
|
|
76
|
-
notifyRetryableError(model, retryable, errorMessage);
|
|
77
|
-
ensureRateLimitedModelsStatus();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function activeNonRetryableFailure(model: Model<Api>) {
|
|
81
|
-
const entry = state.nonRetryableFailureMemory.get(modelKey(model));
|
|
82
|
-
if (!entry) return undefined;
|
|
83
|
-
if (Date.now() >= entry.deadline) {
|
|
84
|
-
state.nonRetryableFailureMemory.delete(modelKey(model));
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
87
|
-
return entry;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function hasNonRetryableFailure(model: Model<Api>): boolean {
|
|
91
|
-
return Boolean(activeNonRetryableFailure(model));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function nextAvailableCandidate(current: Model<Api>): FallbackModel | undefined {
|
|
95
|
-
return candidateOrder(current).find((entry) => !activeLimit(entry.model) && !hasNonRetryableFailure(entry.model));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function rememberNonRetryableFailure(model: Model<Api>, errorMessage: string): void {
|
|
99
|
-
const deadline = Date.now() + DEFAULT_NON_RETRYABLE_FREEZE_MS;
|
|
100
|
-
state.nonRetryableFailureMemory.set(modelKey(model), { failedAt: Date.now(), deadline, errorMessage });
|
|
101
|
-
state.sharedCtx?.ui.notify(`${formatModel(model)} failed (${formatErrorDetail(errorMessage)}); freezing it for ${formatDuration(DEFAULT_NON_RETRYABLE_FREEZE_MS)} and trying another configured model if available.`, "warning");
|
|
102
|
-
ensureRateLimitedModelsStatus();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function recentObservedHttpError(maxAgeMs = 60_000): string | undefined {
|
|
106
|
-
if (!state.lastObservedHttpError) return undefined;
|
|
107
|
-
if (Date.now() - state.lastObservedHttpError.at > maxAgeMs) return undefined;
|
|
108
|
-
return state.lastObservedHttpError.message;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function recentObservedRateLimitError(maxAgeMs = 60_000): string | undefined {
|
|
112
|
-
const message = recentObservedHttpError(maxAgeMs);
|
|
113
|
-
return message && isRateLimitError(message) ? message : undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function errorMessageWithRecentHttpStatus(errorMessage: string): string {
|
|
117
|
-
const observed = recentObservedHttpError();
|
|
118
|
-
if (!observed || errorMessage.includes(observed)) return errorMessage;
|
|
119
|
-
|
|
120
|
-
// SDK errors often keep only "429 Too Many Requests" and drop the response
|
|
121
|
-
// headers. Keep the observed fetch response when it carries retry timing, so
|
|
122
|
-
// Claude subscription limits use their exact reset/retry time instead of the
|
|
123
|
-
// generic 30 minute fallback.
|
|
124
|
-
const currentHasRetryTiming = parseRetryDelayMs(errorMessage) !== undefined;
|
|
125
|
-
const observedHasRetryTiming = parseRetryDelayMs(observed) !== undefined;
|
|
126
|
-
if (!currentHasRetryTiming && observedHasRetryTiming) return `${observed}; Error: ${errorMessage}`;
|
|
127
|
-
|
|
128
|
-
if (/(?:^|\D)[1-5]\d\d(?:\D|$)/.test(errorMessage)) return errorMessage;
|
|
129
|
-
return `${observed}; Error: ${errorMessage}`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function earliestCandidateDeadline(current: Model<Api>): number | undefined {
|
|
133
|
-
const deadlines = candidateOrder(current)
|
|
134
|
-
.map((entry) => activeLimit(entry.model)?.deadline ?? activeNonRetryableFailure(entry.model)?.deadline)
|
|
135
|
-
.filter((deadline): deadline is number => typeof deadline === "number");
|
|
136
|
-
return deadlines.length > 0 ? Math.min(...deadlines) : undefined;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function initialAttempt(model: Model<Api>): FallbackModel {
|
|
140
|
-
const configured = state.fallbackModels.find((entry) => modelKey(entry.model) === modelKey(model));
|
|
141
|
-
const current = configured ?? { model, reasoningEffort: state.primaryThinkingLevel };
|
|
142
|
-
if (!fallbackEnabled() || (!activeLimit(model) && !hasNonRetryableFailure(model))) return current;
|
|
143
|
-
return nextAvailableCandidate(model) ?? current;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export async function optionsForModel(
|
|
147
|
-
originalModel: Model<Api>,
|
|
148
|
-
target: FallbackModel,
|
|
149
|
-
options?: SimpleStreamOptions,
|
|
150
|
-
): Promise<SimpleStreamOptions | undefined> {
|
|
151
|
-
const level = target.reasoningEffort ?? state.primaryThinkingLevel;
|
|
152
|
-
const reasoning = level && level !== "off" ? level : undefined;
|
|
153
|
-
if (modelKey(originalModel) === modelKey(target.model)) {
|
|
154
|
-
return reasoning ? { ...options, reasoning } : options;
|
|
155
|
-
}
|
|
156
|
-
const auth = await state.sharedCtx?.modelRegistry.getApiKeyAndHeaders(target.model);
|
|
157
|
-
if (!auth?.ok) throw new Error(auth ? auth.error : "Model registry is unavailable.");
|
|
158
|
-
return {
|
|
159
|
-
...options,
|
|
160
|
-
...(reasoning ? { reasoning } : {}),
|
|
161
|
-
apiKey: auth.apiKey,
|
|
162
|
-
headers: auth.headers || options?.headers ? { ...auth.headers, ...options?.headers } : undefined,
|
|
163
|
-
} as SimpleStreamOptions;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function switchPiModel(entry: FallbackModel): Promise<void> {
|
|
167
|
-
const pi = state.extensionApi;
|
|
168
|
-
if (!pi) return;
|
|
169
|
-
const current = state.sharedCtx?.model;
|
|
170
|
-
if (current && modelKey(current) === modelKey(entry.model)) return;
|
|
171
|
-
state.suppressNextModelSelect = true;
|
|
172
|
-
const ok = await pi.setModel(entry.model);
|
|
173
|
-
if (!ok) {
|
|
174
|
-
state.suppressNextModelSelect = false;
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const level = entry.reasoningEffort ?? state.primaryThinkingLevel;
|
|
178
|
-
if (level) pi.setThinkingLevel(level);
|
|
179
|
-
state.sharedCtx?.ui.notify(`Switched to ${formatModel(entry.model)}${level ? ` (${level})` : ""}.`, "info");
|
|
180
|
-
}
|
|
1
|
+
import type { Api, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
|
|
2
|
+
import { DEFAULT_NON_RETRYABLE_FREEZE_MS } from "./constants.js";
|
|
3
|
+
import { isRateLimitError, parseRetryDelayMs } from "./retry-errors.js";
|
|
4
|
+
import { state } from "./state.js";
|
|
5
|
+
import type { FallbackModel, RetryableError } from "./types.js";
|
|
6
|
+
import { formatErrorDetail, formatDuration, reasonLabel } from "./ui.js";
|
|
7
|
+
|
|
8
|
+
export function modelKey(model: Pick<Model<Api>, "provider" | "id">): string {
|
|
9
|
+
return `${model.provider}/${model.id}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatModel(model: Pick<Model<Api>, "provider" | "id">): string {
|
|
13
|
+
return `${model.provider}/${model.id}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isInternalSyntheticModel(model: Pick<Model<Api>, "provider" | "id">): boolean {
|
|
17
|
+
return model.provider.startsWith("pi-") || model.id.startsWith("synthetic-");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isFallbackEligibleModel(model: Model<Api>): boolean {
|
|
21
|
+
if (isInternalSyntheticModel(model)) return false;
|
|
22
|
+
const key = modelKey(model);
|
|
23
|
+
return state.primaryModel
|
|
24
|
+
? key === modelKey(state.primaryModel) || state.fallbackModels.some((entry) => modelKey(entry.model) === key)
|
|
25
|
+
: true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fallbackEnabled(): boolean {
|
|
29
|
+
return state.fallbackModels.length > 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getPrimaryModel(current: Model<Api>): Model<Api> {
|
|
33
|
+
return state.primaryModel ?? current;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function candidateOrder(current: Model<Api>): FallbackModel[] {
|
|
37
|
+
const primary = getPrimaryModel(current);
|
|
38
|
+
const order: FallbackModel[] = [{ model: primary, reasoningEffort: state.primaryThinkingLevel }];
|
|
39
|
+
const seen = new Set([modelKey(primary)]);
|
|
40
|
+
for (const entry of state.fallbackModels) {
|
|
41
|
+
const key = modelKey(entry.model);
|
|
42
|
+
if (seen.has(key)) continue;
|
|
43
|
+
seen.add(key);
|
|
44
|
+
order.push(entry);
|
|
45
|
+
}
|
|
46
|
+
return order;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function activeLimit(model: Model<Api>) {
|
|
50
|
+
const entry = state.rateLimitMemory.get(modelKey(model));
|
|
51
|
+
if (!entry) return undefined;
|
|
52
|
+
if (Date.now() >= entry.deadline) {
|
|
53
|
+
state.rateLimitMemory.delete(modelKey(model));
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return entry;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function ensureRateLimitedModelsStatus(): void {
|
|
60
|
+
// Intentionally no-op: limited/frozen model information is communicated via
|
|
61
|
+
// chat notifications only. Do not render persistent TUI status lines.
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function notifyRetryableError(model: Model<Api>, retryable: RetryableError, errorMessage?: string): void {
|
|
65
|
+
const detail = errorMessage ? ` Error: ${formatErrorDetail(errorMessage, Number.MAX_SAFE_INTEGER)}` : "";
|
|
66
|
+
state.sharedCtx?.ui.notify(`${formatModel(model)} ${reasonLabel(retryable.reason).toLowerCase()} for ${formatDuration(retryable.waitMs)}.${detail}`, "warning");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function notifyRetryingAfterError(model: Model<Api>, waitMs: number, errorMessage: string): void {
|
|
70
|
+
state.sharedCtx?.ui.notify(`${formatModel(model)} retrying after error for ${formatDuration(waitMs)}. Error: ${formatErrorDetail(errorMessage, Number.MAX_SAFE_INTEGER)}`, "warning");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function rememberRateLimit(model: Model<Api>, retryable: RetryableError, errorMessage?: string): void {
|
|
74
|
+
const deadline = Date.now() + retryable.waitMs;
|
|
75
|
+
state.rateLimitMemory.set(modelKey(model), { reason: retryable.reason, limitedAt: Date.now(), deadline });
|
|
76
|
+
notifyRetryableError(model, retryable, errorMessage);
|
|
77
|
+
ensureRateLimitedModelsStatus();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function activeNonRetryableFailure(model: Model<Api>) {
|
|
81
|
+
const entry = state.nonRetryableFailureMemory.get(modelKey(model));
|
|
82
|
+
if (!entry) return undefined;
|
|
83
|
+
if (Date.now() >= entry.deadline) {
|
|
84
|
+
state.nonRetryableFailureMemory.delete(modelKey(model));
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return entry;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function hasNonRetryableFailure(model: Model<Api>): boolean {
|
|
91
|
+
return Boolean(activeNonRetryableFailure(model));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function nextAvailableCandidate(current: Model<Api>): FallbackModel | undefined {
|
|
95
|
+
return candidateOrder(current).find((entry) => !activeLimit(entry.model) && !hasNonRetryableFailure(entry.model));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function rememberNonRetryableFailure(model: Model<Api>, errorMessage: string): void {
|
|
99
|
+
const deadline = Date.now() + DEFAULT_NON_RETRYABLE_FREEZE_MS;
|
|
100
|
+
state.nonRetryableFailureMemory.set(modelKey(model), { failedAt: Date.now(), deadline, errorMessage });
|
|
101
|
+
state.sharedCtx?.ui.notify(`${formatModel(model)} failed (${formatErrorDetail(errorMessage)}); freezing it for ${formatDuration(DEFAULT_NON_RETRYABLE_FREEZE_MS)} and trying another configured model if available.`, "warning");
|
|
102
|
+
ensureRateLimitedModelsStatus();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function recentObservedHttpError(maxAgeMs = 60_000): string | undefined {
|
|
106
|
+
if (!state.lastObservedHttpError) return undefined;
|
|
107
|
+
if (Date.now() - state.lastObservedHttpError.at > maxAgeMs) return undefined;
|
|
108
|
+
return state.lastObservedHttpError.message;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function recentObservedRateLimitError(maxAgeMs = 60_000): string | undefined {
|
|
112
|
+
const message = recentObservedHttpError(maxAgeMs);
|
|
113
|
+
return message && isRateLimitError(message) ? message : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function errorMessageWithRecentHttpStatus(errorMessage: string): string {
|
|
117
|
+
const observed = recentObservedHttpError();
|
|
118
|
+
if (!observed || errorMessage.includes(observed)) return errorMessage;
|
|
119
|
+
|
|
120
|
+
// SDK errors often keep only "429 Too Many Requests" and drop the response
|
|
121
|
+
// headers. Keep the observed fetch response when it carries retry timing, so
|
|
122
|
+
// Claude subscription limits use their exact reset/retry time instead of the
|
|
123
|
+
// generic 30 minute fallback.
|
|
124
|
+
const currentHasRetryTiming = parseRetryDelayMs(errorMessage) !== undefined;
|
|
125
|
+
const observedHasRetryTiming = parseRetryDelayMs(observed) !== undefined;
|
|
126
|
+
if (!currentHasRetryTiming && observedHasRetryTiming) return `${observed}; Error: ${errorMessage}`;
|
|
127
|
+
|
|
128
|
+
if (/(?:^|\D)[1-5]\d\d(?:\D|$)/.test(errorMessage)) return errorMessage;
|
|
129
|
+
return `${observed}; Error: ${errorMessage}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function earliestCandidateDeadline(current: Model<Api>): number | undefined {
|
|
133
|
+
const deadlines = candidateOrder(current)
|
|
134
|
+
.map((entry) => activeLimit(entry.model)?.deadline ?? activeNonRetryableFailure(entry.model)?.deadline)
|
|
135
|
+
.filter((deadline): deadline is number => typeof deadline === "number");
|
|
136
|
+
return deadlines.length > 0 ? Math.min(...deadlines) : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function initialAttempt(model: Model<Api>): FallbackModel {
|
|
140
|
+
const configured = state.fallbackModels.find((entry) => modelKey(entry.model) === modelKey(model));
|
|
141
|
+
const current = configured ?? { model, reasoningEffort: state.primaryThinkingLevel };
|
|
142
|
+
if (!fallbackEnabled() || (!activeLimit(model) && !hasNonRetryableFailure(model))) return current;
|
|
143
|
+
return nextAvailableCandidate(model) ?? current;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function optionsForModel(
|
|
147
|
+
originalModel: Model<Api>,
|
|
148
|
+
target: FallbackModel,
|
|
149
|
+
options?: SimpleStreamOptions,
|
|
150
|
+
): Promise<SimpleStreamOptions | undefined> {
|
|
151
|
+
const level = target.reasoningEffort ?? state.primaryThinkingLevel;
|
|
152
|
+
const reasoning = level && level !== "off" ? level : undefined;
|
|
153
|
+
if (modelKey(originalModel) === modelKey(target.model)) {
|
|
154
|
+
return reasoning ? { ...options, reasoning } : options;
|
|
155
|
+
}
|
|
156
|
+
const auth = await state.sharedCtx?.modelRegistry.getApiKeyAndHeaders(target.model);
|
|
157
|
+
if (!auth?.ok) throw new Error(auth ? auth.error : "Model registry is unavailable.");
|
|
158
|
+
return {
|
|
159
|
+
...options,
|
|
160
|
+
...(reasoning ? { reasoning } : {}),
|
|
161
|
+
apiKey: auth.apiKey,
|
|
162
|
+
headers: auth.headers || options?.headers ? { ...auth.headers, ...options?.headers } : undefined,
|
|
163
|
+
} as SimpleStreamOptions;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function switchPiModel(entry: FallbackModel): Promise<void> {
|
|
167
|
+
const pi = state.extensionApi;
|
|
168
|
+
if (!pi) return;
|
|
169
|
+
const current = state.sharedCtx?.model;
|
|
170
|
+
if (current && modelKey(current) === modelKey(entry.model)) return;
|
|
171
|
+
state.suppressNextModelSelect = true;
|
|
172
|
+
const ok = await pi.setModel(entry.model);
|
|
173
|
+
if (!ok) {
|
|
174
|
+
state.suppressNextModelSelect = false;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const level = entry.reasoningEffort ?? state.primaryThinkingLevel;
|
|
178
|
+
if (level) pi.setThinkingLevel(level);
|
|
179
|
+
state.sharedCtx?.ui.notify(`Switched to ${formatModel(entry.model)}${level ? ` (${level})` : ""}.`, "info");
|
|
180
|
+
}
|
package/src/prompt.ts
CHANGED
|
@@ -1,26 +1,41 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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";
|
|
4
|
+
import { state } from "./state.js";
|
|
5
|
+
|
|
6
|
+
export function sanitiseSystemPrompt(raw: string): string {
|
|
7
|
+
const paragraphs = raw.split(/\n\n+/);
|
|
8
|
+
const filtered = paragraphs.filter((p) =>
|
|
9
|
+
!PI_REMOVAL_ANCHORS.some((anchor) => p.includes(anchor)),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
return filtered
|
|
13
|
+
.join("\n\n")
|
|
14
|
+
.replace(CLAUDE_CODE_IDENTITY_PATTERN, "")
|
|
15
|
+
.replace(PI_IDENTITY_SENTENCE_PATTERN, "")
|
|
16
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
17
|
+
.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns true if Anthropic OAuth is configured for this session, regardless
|
|
22
|
+
* of which model is currently active. This handles cases where a synthetic or
|
|
23
|
+
* temporary model is active at before_agent_start time.
|
|
24
|
+
*/
|
|
25
|
+
export function isAnthropicOAuthSession(ctx: ExtensionContext): boolean {
|
|
26
|
+
return ctx.modelRegistry.isUsingOAuth(
|
|
27
|
+
{ provider: "anthropic" } as Parameters<typeof ctx.modelRegistry.isUsingOAuth>[0],
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function anthropicSubscriptionContext(model: Model<Api>, context: Context): Context {
|
|
32
|
+
if (model.provider !== "anthropic" || !state.sharedCtx?.modelRegistry.isUsingOAuth(model)) return context;
|
|
33
|
+
|
|
34
|
+
const sanitised = sanitiseSystemPrompt(context.systemPrompt ?? "");
|
|
35
|
+
return {
|
|
36
|
+
...context,
|
|
37
|
+
systemPrompt: sanitised
|
|
38
|
+
? `${CLAUDE_CODE_IDENTITY}\n\n${sanitised}`
|
|
39
|
+
: CLAUDE_CODE_IDENTITY,
|
|
40
|
+
};
|
|
41
|
+
}
|