@oh-my-pi/pi-ai 8.0.20 → 8.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/README.md +11 -12
- package/package.json +49 -26
- package/src/cli.ts +7 -7
- package/src/index.ts +2 -1
- package/src/models.generated.ts +100 -101
- package/src/providers/amazon-bedrock.ts +12 -13
- package/src/providers/anthropic.ts +67 -37
- package/src/providers/cursor.ts +57 -57
- package/src/providers/google-gemini-cli-usage.ts +2 -2
- package/src/providers/google-gemini-cli.ts +8 -10
- package/src/providers/google-shared.ts +12 -13
- package/src/providers/google-vertex.ts +7 -7
- package/src/providers/google.ts +8 -8
- package/src/providers/openai-codex/request-transformer.ts +6 -6
- package/src/providers/openai-codex-responses.ts +28 -28
- package/src/providers/openai-completions.ts +39 -39
- package/src/providers/openai-responses.ts +31 -31
- package/src/providers/transform-messages.ts +3 -3
- package/src/storage.ts +29 -19
- package/src/stream.ts +6 -6
- package/src/types.ts +1 -2
- package/src/usage/claude.ts +4 -4
- package/src/usage/github-copilot.ts +3 -4
- package/src/usage/google-antigravity.ts +3 -3
- package/src/usage/openai-codex.ts +4 -4
- package/src/usage/zai.ts +3 -3
- package/src/usage.ts +0 -1
- package/src/utils/event-stream.ts +4 -4
- package/src/utils/oauth/anthropic.ts +0 -1
- package/src/utils/oauth/callback-server.ts +2 -3
- package/src/utils/oauth/github-copilot.ts +2 -3
- package/src/utils/oauth/google-antigravity.ts +0 -1
- package/src/utils/oauth/google-gemini-cli.ts +2 -3
- package/src/utils/oauth/index.ts +11 -12
- package/src/utils/oauth/openai-codex.ts +0 -1
- package/src/utils/overflow.ts +2 -2
- package/src/utils/retry.ts +78 -0
- package/src/utils/validation.ts +4 -5
- package/tsconfig.json +0 -42
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
import { calculateCost } from "@oh-my-pi/pi-ai/models";
|
|
2
|
-
import { getEnvApiKey } from "@oh-my-pi/pi-ai/stream";
|
|
3
|
-
import type {
|
|
4
|
-
Api,
|
|
5
|
-
AssistantMessage,
|
|
6
|
-
Context,
|
|
7
|
-
Model,
|
|
8
|
-
StopReason,
|
|
9
|
-
StreamFunction,
|
|
10
|
-
StreamOptions,
|
|
11
|
-
TextContent,
|
|
12
|
-
ThinkingContent,
|
|
13
|
-
Tool,
|
|
14
|
-
ToolCall,
|
|
15
|
-
} from "@oh-my-pi/pi-ai/types";
|
|
16
|
-
import { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
|
|
17
|
-
import { parseStreamingJson } from "@oh-my-pi/pi-ai/utils/json-parse";
|
|
18
|
-
import { formatErrorMessageWithRetryAfter } from "@oh-my-pi/pi-ai/utils/retry-after";
|
|
19
|
-
import { sanitizeSurrogates } from "@oh-my-pi/pi-ai/utils/sanitize-unicode";
|
|
20
1
|
import OpenAI from "openai";
|
|
21
2
|
import type {
|
|
22
3
|
Tool as OpenAITool,
|
|
@@ -29,6 +10,25 @@ import type {
|
|
|
29
10
|
ResponseOutputMessage,
|
|
30
11
|
ResponseReasoningItem,
|
|
31
12
|
} from "openai/resources/responses/responses";
|
|
13
|
+
import { calculateCost } from "../models";
|
|
14
|
+
import { getEnvApiKey } from "../stream";
|
|
15
|
+
import type {
|
|
16
|
+
Api,
|
|
17
|
+
AssistantMessage,
|
|
18
|
+
Context,
|
|
19
|
+
Model,
|
|
20
|
+
StopReason,
|
|
21
|
+
StreamFunction,
|
|
22
|
+
StreamOptions,
|
|
23
|
+
TextContent,
|
|
24
|
+
ThinkingContent,
|
|
25
|
+
Tool,
|
|
26
|
+
ToolCall,
|
|
27
|
+
} from "../types";
|
|
28
|
+
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
29
|
+
import { parseStreamingJson } from "../utils/json-parse";
|
|
30
|
+
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
31
|
+
import { sanitizeSurrogates } from "../utils/sanitize-unicode";
|
|
32
32
|
import { transformMessages } from "./transform-messages";
|
|
33
33
|
|
|
34
34
|
/** Fast deterministic hash to shorten long strings */
|
|
@@ -245,7 +245,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
245
245
|
const item = event.item;
|
|
246
246
|
|
|
247
247
|
if (item.type === "reasoning" && currentBlock && currentBlock.type === "thinking") {
|
|
248
|
-
currentBlock.thinking = item.summary?.map(
|
|
248
|
+
currentBlock.thinking = item.summary?.map(s => s.text).join("\n\n") || "";
|
|
249
249
|
currentBlock.thinkingSignature = JSON.stringify(item);
|
|
250
250
|
stream.push({
|
|
251
251
|
type: "thinking_end",
|
|
@@ -255,7 +255,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
255
255
|
});
|
|
256
256
|
currentBlock = null;
|
|
257
257
|
} else if (item.type === "message" && currentBlock && currentBlock.type === "text") {
|
|
258
|
-
currentBlock.text = item.content.map(
|
|
258
|
+
currentBlock.text = item.content.map(c => (c.type === "output_text" ? c.text : c.refusal)).join("");
|
|
259
259
|
currentBlock.textSignature = item.id;
|
|
260
260
|
stream.push({
|
|
261
261
|
type: "text_end",
|
|
@@ -293,7 +293,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
293
293
|
calculateCost(model, output.usage);
|
|
294
294
|
// Map status to stop reason
|
|
295
295
|
output.stopReason = mapStopReason(response?.status);
|
|
296
|
-
if (output.content.some(
|
|
296
|
+
if (output.content.some(b => b.type === "toolCall") && output.stopReason === "stop") {
|
|
297
297
|
output.stopReason = "toolUse";
|
|
298
298
|
}
|
|
299
299
|
}
|
|
@@ -358,12 +358,12 @@ function createClient(
|
|
|
358
358
|
headers["Openai-Intent"] = "conversation-edits";
|
|
359
359
|
|
|
360
360
|
// Copilot requires this header when sending images
|
|
361
|
-
const hasImages = messages.some(
|
|
361
|
+
const hasImages = messages.some(msg => {
|
|
362
362
|
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
363
|
-
return msg.content.some(
|
|
363
|
+
return msg.content.some(c => c.type === "image");
|
|
364
364
|
}
|
|
365
365
|
if (msg.role === "toolResult" && Array.isArray(msg.content)) {
|
|
366
|
-
return msg.content.some(
|
|
366
|
+
return msg.content.some(c => c.type === "image");
|
|
367
367
|
}
|
|
368
368
|
return false;
|
|
369
369
|
});
|
|
@@ -491,9 +491,9 @@ function convertMessages(
|
|
|
491
491
|
});
|
|
492
492
|
// Filter out images if model doesn't support them, and empty text blocks
|
|
493
493
|
let filteredContent = !model.input.includes("image")
|
|
494
|
-
? content.filter(
|
|
494
|
+
? content.filter(c => c.type !== "input_image")
|
|
495
495
|
: content;
|
|
496
|
-
filteredContent = filteredContent.filter(
|
|
496
|
+
filteredContent = filteredContent.filter(c => {
|
|
497
497
|
if (c.type === "input_text") {
|
|
498
498
|
return c.text.trim().length > 0;
|
|
499
499
|
}
|
|
@@ -567,10 +567,10 @@ function convertMessages(
|
|
|
567
567
|
} else if (msg.role === "toolResult") {
|
|
568
568
|
// Extract text and image content
|
|
569
569
|
const textResult = msg.content
|
|
570
|
-
.filter(
|
|
571
|
-
.map(
|
|
570
|
+
.filter(c => c.type === "text")
|
|
571
|
+
.map(c => (c as any).text)
|
|
572
572
|
.join("\n");
|
|
573
|
-
const hasImages = msg.content.some(
|
|
573
|
+
const hasImages = msg.content.some(c => c.type === "image");
|
|
574
574
|
const normalized = normalizeResponsesToolCallId(msg.toolCallId);
|
|
575
575
|
if (strictResponsesPairing && !knownCallIds.has(normalized.callId)) {
|
|
576
576
|
continue;
|
|
@@ -618,7 +618,7 @@ function convertMessages(
|
|
|
618
618
|
}
|
|
619
619
|
|
|
620
620
|
function convertTools(tools: Tool[]): OpenAITool[] {
|
|
621
|
-
return tools.map(
|
|
621
|
+
return tools.map(tool => ({
|
|
622
622
|
type: "function",
|
|
623
623
|
name: tool.name,
|
|
624
624
|
description: tool.description,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Api, AssistantMessage, Message, Model, ToolCall, ToolResultMessage } from "
|
|
1
|
+
import type { Api, AssistantMessage, Message, Model, ToolCall, ToolResultMessage } from "../types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Normalize tool call ID for cross-provider compatibility.
|
|
@@ -98,7 +98,7 @@ export function transformMessages<TApi extends Api>(messages: Message[], model:
|
|
|
98
98
|
const needsToolCallIdNormalization = targetRequiresStrictIds && (crossProviderSwitch || copilotCrossApiSwitch);
|
|
99
99
|
|
|
100
100
|
// Transform message from different provider/model
|
|
101
|
-
const transformedContent = assistantMsg.content.flatMap(
|
|
101
|
+
const transformedContent = assistantMsg.content.flatMap(block => {
|
|
102
102
|
if (block.type === "thinking") {
|
|
103
103
|
// Skip empty thinking blocks, convert others to plain text
|
|
104
104
|
if (!block.thinking || block.thinking.trim() === "") return [];
|
|
@@ -173,7 +173,7 @@ export function transformMessages<TApi extends Api>(messages: Message[], model:
|
|
|
173
173
|
|
|
174
174
|
const assistantMsg = msg as AssistantMessage;
|
|
175
175
|
const isErroredAssistant = assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted";
|
|
176
|
-
const toolCalls = assistantMsg.content.filter(
|
|
176
|
+
const toolCalls = assistantMsg.content.filter(b => b.type === "toolCall") as ToolCall[];
|
|
177
177
|
|
|
178
178
|
result.push(msg);
|
|
179
179
|
|
package/src/storage.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Database } from "bun:sqlite";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
10
|
import type { OAuthCredentials } from "./utils/oauth/types";
|
|
11
11
|
|
|
12
12
|
type AuthCredential = { type: "api_key"; key: string } | ({ type: "oauth" } & OAuthCredentials);
|
|
@@ -24,7 +24,7 @@ type AuthRow = {
|
|
|
24
24
|
* Get the agent config directory (e.g., ~/.omp/agent/)
|
|
25
25
|
*/
|
|
26
26
|
function getAgentDir(): string {
|
|
27
|
-
const configDir = process.env.OMP_CODING_AGENT_DIR || join(homedir(), ".omp", "agent");
|
|
27
|
+
const configDir = process.env.OMP_CODING_AGENT_DIR || path.join(os.homedir(), ".omp", "agent");
|
|
28
28
|
return configDir;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -32,7 +32,7 @@ function getAgentDir(): string {
|
|
|
32
32
|
* Get path to agent.db
|
|
33
33
|
*/
|
|
34
34
|
function getAgentDbPath(): string {
|
|
35
|
-
return join(getAgentDir(), "agent.db");
|
|
35
|
+
return path.join(getAgentDir(), "agent.db");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function serializeCredential(credential: AuthCredential): { credentialType: string; data: string } | null {
|
|
@@ -79,6 +79,8 @@ function deserializeCredential(row: AuthRow): AuthCredential | null {
|
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Simple storage class for CLI auth credentials.
|
|
82
|
+
*
|
|
83
|
+
* Use `CliAuthStorage.create()` to instantiate (async initialization).
|
|
82
84
|
*/
|
|
83
85
|
export class CliAuthStorage {
|
|
84
86
|
private db: Database;
|
|
@@ -87,20 +89,8 @@ export class CliAuthStorage {
|
|
|
87
89
|
private listAllStmt: ReturnType<Database["prepare"]>;
|
|
88
90
|
private deleteByProviderStmt: ReturnType<Database["prepare"]>;
|
|
89
91
|
|
|
90
|
-
constructor(
|
|
91
|
-
|
|
92
|
-
const dir = dirname(dbPath);
|
|
93
|
-
if (!existsSync(dir)) {
|
|
94
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
this.db = new Database(dbPath);
|
|
98
|
-
// Harden database file permissions to prevent credential leakage
|
|
99
|
-
try {
|
|
100
|
-
chmodSync(dbPath, 0o600);
|
|
101
|
-
} catch {
|
|
102
|
-
// Ignore chmod failures (e.g., Windows)
|
|
103
|
-
}
|
|
92
|
+
private constructor(db: Database) {
|
|
93
|
+
this.db = db;
|
|
104
94
|
this.initializeSchema();
|
|
105
95
|
|
|
106
96
|
this.insertStmt = this.db.prepare(
|
|
@@ -111,6 +101,26 @@ export class CliAuthStorage {
|
|
|
111
101
|
this.deleteByProviderStmt = this.db.prepare("DELETE FROM auth_credentials WHERE provider = ?");
|
|
112
102
|
}
|
|
113
103
|
|
|
104
|
+
static async create(dbPath: string = getAgentDbPath()): Promise<CliAuthStorage> {
|
|
105
|
+
const dir = path.dirname(dbPath);
|
|
106
|
+
const dirExists = await fs
|
|
107
|
+
.stat(dir)
|
|
108
|
+
.then(s => s.isDirectory())
|
|
109
|
+
.catch(() => false);
|
|
110
|
+
if (!dirExists) {
|
|
111
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const db = new Database(dbPath);
|
|
115
|
+
try {
|
|
116
|
+
await fs.chmod(dbPath, 0o600);
|
|
117
|
+
} catch {
|
|
118
|
+
// Ignore chmod failures (e.g., Windows)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return new CliAuthStorage(db);
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
private initializeSchema(): void {
|
|
115
125
|
this.db.exec(`
|
|
116
126
|
PRAGMA journal_mode=WAL;
|
package/src/stream.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
4
|
import { supportsXhigh } from "./models";
|
|
5
5
|
import { type BedrockOptions, streamBedrock } from "./providers/amazon-bedrock";
|
|
6
6
|
import { type AnthropicOptions, streamAnthropic } from "./providers/anthropic";
|
|
@@ -34,10 +34,10 @@ function hasVertexAdcCredentials(): boolean {
|
|
|
34
34
|
if (cachedVertexAdcCredentialsExists === null) {
|
|
35
35
|
const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
36
36
|
if (gacPath) {
|
|
37
|
-
cachedVertexAdcCredentialsExists = existsSync(gacPath);
|
|
37
|
+
cachedVertexAdcCredentialsExists = fs.existsSync(gacPath);
|
|
38
38
|
} else {
|
|
39
|
-
cachedVertexAdcCredentialsExists = existsSync(
|
|
40
|
-
join(homedir(), ".config", "gcloud", "application_default_credentials.json"),
|
|
39
|
+
cachedVertexAdcCredentialsExists = fs.existsSync(
|
|
40
|
+
path.join(os.homedir(), ".config", "gcloud", "application_default_credentials.json"),
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
43
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
1
2
|
import type { BedrockOptions } from "./providers/amazon-bedrock";
|
|
2
3
|
import type { AnthropicOptions } from "./providers/anthropic";
|
|
3
4
|
import type { CursorOptions } from "./providers/cursor";
|
|
@@ -237,8 +238,6 @@ export interface CursorExecHandlers {
|
|
|
237
238
|
onToolResult?: CursorToolResultHandler;
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
import type { TSchema } from "@sinclair/typebox";
|
|
241
|
-
|
|
242
241
|
export interface Tool<TParameters extends TSchema = TSchema> {
|
|
243
242
|
name: string;
|
|
244
243
|
description: string;
|
package/src/usage/claude.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
UsageReport,
|
|
8
8
|
UsageStatus,
|
|
9
9
|
UsageWindow,
|
|
10
|
-
} from "
|
|
10
|
+
} from "../usage";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_ENDPOINT = "https://api.anthropic.com/api/oauth";
|
|
13
13
|
const DEFAULT_CACHE_TTL_MS = 60_000;
|
|
@@ -241,10 +241,10 @@ function buildCacheKey(params: UsageFetchParams): string {
|
|
|
241
241
|
|
|
242
242
|
function resolveCacheExpiry(now: number, limits: UsageLimit[]): number {
|
|
243
243
|
const earliestReset = limits
|
|
244
|
-
.map(
|
|
244
|
+
.map(limit => limit.window?.resetsAt)
|
|
245
245
|
.filter((value): value is number => typeof value === "number" && Number.isFinite(value))
|
|
246
246
|
.reduce((min, value) => (min === undefined ? value : Math.min(min, value)), undefined as number | undefined);
|
|
247
|
-
const exhausted = limits.some(
|
|
247
|
+
const exhausted = limits.some(limit => limit.status === "exhausted");
|
|
248
248
|
if (earliestReset === undefined) return now + DEFAULT_CACHE_TTL_MS;
|
|
249
249
|
if (exhausted) return earliestReset;
|
|
250
250
|
return Math.min(now + DEFAULT_CACHE_TTL_MS, earliestReset);
|
|
@@ -351,5 +351,5 @@ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext
|
|
|
351
351
|
export const claudeUsageProvider: UsageProvider = {
|
|
352
352
|
id: "anthropic",
|
|
353
353
|
fetchUsage: fetchClaudeUsage,
|
|
354
|
-
supports:
|
|
354
|
+
supports: params => params.provider === "anthropic" && params.credential.type === "oauth",
|
|
355
355
|
};
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Normalizes Copilot quota usage into the shared UsageReport schema.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
6
|
import type {
|
|
8
7
|
UsageAmount,
|
|
9
8
|
UsageCacheEntry,
|
|
@@ -14,7 +13,7 @@ import type {
|
|
|
14
13
|
UsageReport,
|
|
15
14
|
UsageStatus,
|
|
16
15
|
UsageWindow,
|
|
17
|
-
} from "
|
|
16
|
+
} from "../usage";
|
|
18
17
|
|
|
19
18
|
const COPILOT_HEADERS = {
|
|
20
19
|
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
@@ -316,7 +315,7 @@ function normalizeBillingUsage(data: BillingUsageResponse): UsageLimit[] {
|
|
|
316
315
|
};
|
|
317
316
|
|
|
318
317
|
const premiumItems = data.usageItems.filter(
|
|
319
|
-
|
|
318
|
+
item => item.sku === "Copilot Premium Request" || item.sku.includes("Premium"),
|
|
320
319
|
);
|
|
321
320
|
const totalUsed = premiumItems.reduce((sum, item) => sum + item.grossQuantity, 0);
|
|
322
321
|
const totalLimit = premiumItems.reduce((sum, item) => sum + (item.limit ?? 0), 0) || undefined;
|
|
@@ -359,7 +358,7 @@ function normalizeBillingUsage(data: BillingUsageResponse): UsageLimit[] {
|
|
|
359
358
|
function resolveCacheTtl(now: number, report: UsageReport | null): UsageCacheEntry["expiresAt"] {
|
|
360
359
|
if (!report) return now + DEFAULT_CACHE_TTL_MS;
|
|
361
360
|
const resetInMs = report.limits
|
|
362
|
-
.map(
|
|
361
|
+
.map(limit => limit.window?.resetInMs)
|
|
363
362
|
.find((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
364
363
|
if (!resetInMs || resetInMs <= 0) return now + DEFAULT_CACHE_TTL_MS;
|
|
365
364
|
return now + Math.min(MAX_CACHE_TTL_MS, resetInMs);
|
|
@@ -7,8 +7,8 @@ import type {
|
|
|
7
7
|
UsageReport,
|
|
8
8
|
UsageStatus,
|
|
9
9
|
UsageWindow,
|
|
10
|
-
} from "
|
|
11
|
-
import { refreshAntigravityToken } from "
|
|
10
|
+
} from "../usage";
|
|
11
|
+
import { refreshAntigravityToken } from "../utils/oauth/google-antigravity";
|
|
12
12
|
|
|
13
13
|
interface AntigravityQuotaInfo {
|
|
14
14
|
remainingFraction?: number;
|
|
@@ -214,5 +214,5 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
|
|
|
214
214
|
export const antigravityUsageProvider: UsageProvider = {
|
|
215
215
|
id: "google-antigravity",
|
|
216
216
|
fetchUsage: fetchAntigravityUsage,
|
|
217
|
-
supports:
|
|
217
|
+
supports: params => params.provider === "google-antigravity",
|
|
218
218
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import { CODEX_BASE_URL } from "
|
|
2
|
+
import { CODEX_BASE_URL } from "../providers/openai-codex/constants";
|
|
3
3
|
import type {
|
|
4
4
|
UsageAmount,
|
|
5
5
|
UsageCache,
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
UsageProvider,
|
|
10
10
|
UsageReport,
|
|
11
11
|
UsageWindow,
|
|
12
|
-
} from "
|
|
12
|
+
} from "../usage";
|
|
13
13
|
|
|
14
14
|
const CODEX_USAGE_PATH = "wham/usage";
|
|
15
15
|
const DEFAULT_CACHE_TTL_MS = 60_000;
|
|
@@ -264,9 +264,9 @@ function buildUsageLimit(args: {
|
|
|
264
264
|
function resolveCacheExpiry(args: { report: UsageReport | null; nowMs: number }): number {
|
|
265
265
|
const { report, nowMs } = args;
|
|
266
266
|
if (!report) return nowMs + DEFAULT_CACHE_TTL_MS;
|
|
267
|
-
const exhausted = report.limits.some(
|
|
267
|
+
const exhausted = report.limits.some(limit => limit.status === "exhausted");
|
|
268
268
|
const resetCandidates = report.limits
|
|
269
|
-
.map(
|
|
269
|
+
.map(limit => limit.window?.resetsAt)
|
|
270
270
|
.filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
271
271
|
const earliestReset = resetCandidates.length > 0 ? Math.min(...resetCandidates) : undefined;
|
|
272
272
|
if (exhausted && earliestReset) return earliestReset;
|
package/src/usage/zai.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
UsageReport,
|
|
8
8
|
UsageStatus,
|
|
9
9
|
UsageWindow,
|
|
10
|
-
} from "
|
|
10
|
+
} from "../usage";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_ENDPOINT = "https://api.z.ai";
|
|
13
13
|
const QUOTA_PATH = "/api/monitor/usage/quota/limit";
|
|
@@ -133,7 +133,7 @@ function buildCacheKey(params: UsageFetchParams): string {
|
|
|
133
133
|
|
|
134
134
|
function resolveCacheExpiry(now: number, limits: UsageLimit[]): number {
|
|
135
135
|
const earliestReset = limits
|
|
136
|
-
.map(
|
|
136
|
+
.map(limit => limit.window?.resetsAt)
|
|
137
137
|
.filter((value): value is number => typeof value === "number" && Number.isFinite(value))
|
|
138
138
|
.reduce((min, value) => (min === undefined ? value : Math.min(min, value)), undefined as number | undefined);
|
|
139
139
|
if (!earliestReset) return now + DEFAULT_CACHE_TTL_MS;
|
|
@@ -288,5 +288,5 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
288
288
|
export const zaiUsageProvider: UsageProvider = {
|
|
289
289
|
id: "zai",
|
|
290
290
|
fetchUsage: fetchZaiUsage,
|
|
291
|
-
supports:
|
|
291
|
+
supports: params => params.provider === "zai" && params.credential.type === "api_key",
|
|
292
292
|
};
|
package/src/usage.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Provides a normalized schema to represent multiple limit windows, model tiers,
|
|
5
5
|
* and shared quotas across providers.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
7
|
import type { Provider } from "./types";
|
|
9
8
|
|
|
10
9
|
export type UsageUnit = "percent" | "tokens" | "requests" | "usd" | "minutes" | "bytes" | "unknown";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantMessage, AssistantMessageEvent } from "
|
|
1
|
+
import type { AssistantMessage, AssistantMessageEvent } from "../types";
|
|
2
2
|
|
|
3
3
|
// Generic event stream class for async iteration
|
|
4
4
|
export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
@@ -53,7 +53,7 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
53
53
|
} else if (this.done) {
|
|
54
54
|
return;
|
|
55
55
|
} else {
|
|
56
|
-
const result = await new Promise<IteratorResult<T>>(
|
|
56
|
+
const result = await new Promise<IteratorResult<T>>(resolve => this.waiting.push(resolve));
|
|
57
57
|
if (result.done) return;
|
|
58
58
|
yield result.value;
|
|
59
59
|
}
|
|
@@ -68,8 +68,8 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
68
68
|
export class AssistantMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
|
|
69
69
|
constructor() {
|
|
70
70
|
super(
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
event => event.type === "done" || event.type === "error",
|
|
72
|
+
event => {
|
|
73
73
|
if (event.type === "done") {
|
|
74
74
|
return event.message;
|
|
75
75
|
} else if (event.type === "error") {
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* - generateAuthUrl(): Build provider-specific authorization URL
|
|
11
11
|
* - exchangeToken(): Exchange authorization code for tokens
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
13
|
import templateHtml from "./oauth.html" with { type: "text" };
|
|
15
14
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
16
15
|
|
|
@@ -63,7 +62,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
63
62
|
const bytes = new Uint8Array(16);
|
|
64
63
|
crypto.getRandomValues(bytes);
|
|
65
64
|
return Array.from(bytes)
|
|
66
|
-
.map(
|
|
65
|
+
.map(value => value.toString(16).padStart(2, "0"))
|
|
67
66
|
.join("");
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -125,7 +124,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
125
124
|
hostname: DEFAULT_HOSTNAME,
|
|
126
125
|
port,
|
|
127
126
|
reusePort: false,
|
|
128
|
-
fetch:
|
|
127
|
+
fetch: req => this.handleCallback(req, expectedState),
|
|
129
128
|
});
|
|
130
129
|
}
|
|
131
130
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GitHub Copilot OAuth flow
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
import { getModels } from "@oh-my-pi/pi-ai/models";
|
|
6
4
|
import { abortableSleep } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { getModels } from "../../models";
|
|
7
6
|
import type { OAuthCredentials } from "./types";
|
|
8
7
|
|
|
9
8
|
const decode = (s: string) => atob(s);
|
|
@@ -279,7 +278,7 @@ async function enableAllGitHubCopilotModels(
|
|
|
279
278
|
): Promise<void> {
|
|
280
279
|
const models = getModels("github-copilot");
|
|
281
280
|
await Promise.all(
|
|
282
|
-
models.map(async
|
|
281
|
+
models.map(async model => {
|
|
283
282
|
const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);
|
|
284
283
|
onProgress?.(model.id, success);
|
|
285
284
|
}),
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud)
|
|
3
3
|
* Uses different OAuth credentials than google-gemini-cli for access to additional models.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
5
|
import { OAuthCallbackFlow } from "./callback-server";
|
|
7
6
|
import { generatePKCE } from "./pkce";
|
|
8
7
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Gemini CLI OAuth flow (Google Cloud Code Assist)
|
|
3
3
|
* Standard Gemini models only (gemini-2.0-flash, gemini-2.5-*)
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
5
|
import { OAuthCallbackFlow } from "./callback-server";
|
|
7
6
|
import { generatePKCE } from "./pkce";
|
|
8
7
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
@@ -49,7 +48,7 @@ interface GoogleRpcErrorResponse {
|
|
|
49
48
|
|
|
50
49
|
function getDefaultTier(allowedTiers?: Array<{ id?: string; isDefault?: boolean }>): { id?: string } {
|
|
51
50
|
if (!allowedTiers || allowedTiers.length === 0) return { id: TIER_LEGACY };
|
|
52
|
-
const defaultTier = allowedTiers.find(
|
|
51
|
+
const defaultTier = allowedTiers.find(t => t.isDefault);
|
|
53
52
|
return defaultTier ?? { id: TIER_LEGACY };
|
|
54
53
|
}
|
|
55
54
|
|
|
@@ -58,7 +57,7 @@ function isVpcScAffectedUser(payload: unknown): boolean {
|
|
|
58
57
|
if (!("error" in payload)) return false;
|
|
59
58
|
const error = (payload as GoogleRpcErrorResponse).error;
|
|
60
59
|
if (!error?.details || !Array.isArray(error.details)) return false;
|
|
61
|
-
return error.details.some(
|
|
60
|
+
return error.details.some(detail => detail.reason === "SECURITY_POLICY_VIOLATED");
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
async function pollOperation(
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// High-level API
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { refreshAnthropicToken } from "./anthropic";
|
|
5
|
+
import { refreshCursorToken } from "./cursor";
|
|
6
|
+
import { refreshGitHubCopilotToken } from "./github-copilot";
|
|
7
|
+
import { refreshAntigravityToken } from "./google-antigravity";
|
|
8
|
+
import { refreshGoogleCloudToken } from "./google-gemini-cli";
|
|
9
|
+
import { refreshOpenAICodexToken } from "./openai-codex";
|
|
10
|
+
import type { OAuthCredentials, OAuthProvider, OAuthProviderInfo } from "./types";
|
|
11
|
+
|
|
1
12
|
/**
|
|
2
13
|
* OAuth credential management for AI providers.
|
|
3
14
|
*
|
|
@@ -36,18 +47,6 @@ export { loginOpenAICodex, refreshOpenAICodexToken } from "./openai-codex";
|
|
|
36
47
|
|
|
37
48
|
export * from "./types";
|
|
38
49
|
|
|
39
|
-
// ============================================================================
|
|
40
|
-
// High-level API
|
|
41
|
-
// ============================================================================
|
|
42
|
-
|
|
43
|
-
import { refreshAnthropicToken } from "./anthropic";
|
|
44
|
-
import { refreshCursorToken } from "./cursor";
|
|
45
|
-
import { refreshGitHubCopilotToken } from "./github-copilot";
|
|
46
|
-
import { refreshAntigravityToken } from "./google-antigravity";
|
|
47
|
-
import { refreshGoogleCloudToken } from "./google-gemini-cli";
|
|
48
|
-
import { refreshOpenAICodexToken } from "./openai-codex";
|
|
49
|
-
import type { OAuthCredentials, OAuthProvider, OAuthProviderInfo } from "./types";
|
|
50
|
-
|
|
51
50
|
/**
|
|
52
51
|
* Refresh token for any OAuth provider.
|
|
53
52
|
* Saves the new credentials and returns the new access token.
|
package/src/utils/overflow.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantMessage } from "
|
|
1
|
+
import type { AssistantMessage } from "../types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Regex patterns to detect context overflow errors from different providers.
|
|
@@ -85,7 +85,7 @@ export function isContextOverflow(message: AssistantMessage, contextWindow?: num
|
|
|
85
85
|
// Case 1: Check error message patterns
|
|
86
86
|
if (message.stopReason === "error" && message.errorMessage) {
|
|
87
87
|
// Check known patterns
|
|
88
|
-
if (OVERFLOW_PATTERNS.some(
|
|
88
|
+
if (OVERFLOW_PATTERNS.some(p => p.test(message.errorMessage!))) {
|
|
89
89
|
return true;
|
|
90
90
|
}
|
|
91
91
|
|