@oh-my-pi/pi-ai 1.337.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.
@@ -0,0 +1,143 @@
1
+ /**
2
+ * OAuth credential management for AI providers.
3
+ *
4
+ * This module handles login, token refresh, and credential storage
5
+ * for OAuth-based providers:
6
+ * - Anthropic (Claude Pro/Max)
7
+ * - GitHub Copilot
8
+ * - Google Cloud Code Assist (Gemini CLI)
9
+ * - Antigravity (Gemini 3, Claude, GPT-OSS via Google Cloud)
10
+ */
11
+
12
+ // Anthropic
13
+ export { loginAnthropic, refreshAnthropicToken } from "./anthropic.js";
14
+ // GitHub Copilot
15
+ export {
16
+ getGitHubCopilotBaseUrl,
17
+ loginGitHubCopilot,
18
+ normalizeDomain,
19
+ refreshGitHubCopilotToken,
20
+ } from "./github-copilot.js";
21
+ // Google Antigravity
22
+ export {
23
+ loginAntigravity,
24
+ refreshAntigravityToken,
25
+ } from "./google-antigravity.js";
26
+ // Google Gemini CLI
27
+ export {
28
+ loginGeminiCli,
29
+ refreshGoogleCloudToken,
30
+ } from "./google-gemini-cli.js";
31
+
32
+ export * from "./types.js";
33
+
34
+ // ============================================================================
35
+ // High-level API
36
+ // ============================================================================
37
+
38
+ import { refreshAnthropicToken } from "./anthropic.js";
39
+ import { refreshGitHubCopilotToken } from "./github-copilot.js";
40
+ import { refreshAntigravityToken } from "./google-antigravity.js";
41
+ import { refreshGoogleCloudToken } from "./google-gemini-cli.js";
42
+ import type { OAuthCredentials, OAuthProvider, OAuthProviderInfo } from "./types.js";
43
+
44
+ /**
45
+ * Refresh token for any OAuth provider.
46
+ * Saves the new credentials and returns the new access token.
47
+ */
48
+ export async function refreshOAuthToken(
49
+ provider: OAuthProvider,
50
+ credentials: OAuthCredentials,
51
+ ): Promise<OAuthCredentials> {
52
+ if (!credentials) {
53
+ throw new Error(`No OAuth credentials found for ${provider}`);
54
+ }
55
+
56
+ let newCredentials: OAuthCredentials;
57
+
58
+ switch (provider) {
59
+ case "anthropic":
60
+ newCredentials = await refreshAnthropicToken(credentials.refresh);
61
+ break;
62
+ case "github-copilot":
63
+ newCredentials = await refreshGitHubCopilotToken(credentials.refresh, credentials.enterpriseUrl);
64
+ break;
65
+ case "google-gemini-cli":
66
+ if (!credentials.projectId) {
67
+ throw new Error("Google Cloud credentials missing projectId");
68
+ }
69
+ newCredentials = await refreshGoogleCloudToken(credentials.refresh, credentials.projectId);
70
+ break;
71
+ case "google-antigravity":
72
+ if (!credentials.projectId) {
73
+ throw new Error("Antigravity credentials missing projectId");
74
+ }
75
+ newCredentials = await refreshAntigravityToken(credentials.refresh, credentials.projectId);
76
+ break;
77
+ default:
78
+ throw new Error(`Unknown OAuth provider: ${provider}`);
79
+ }
80
+
81
+ return newCredentials;
82
+ }
83
+
84
+ /**
85
+ * Get API key for a provider from OAuth credentials.
86
+ * Automatically refreshes expired tokens.
87
+ *
88
+ * For google-gemini-cli and antigravity, returns JSON-encoded { token, projectId }
89
+ *
90
+ * @returns API key string, or null if no credentials
91
+ * @throws Error if refresh fails
92
+ */
93
+ export async function getOAuthApiKey(
94
+ provider: OAuthProvider,
95
+ credentials: Record<string, OAuthCredentials>,
96
+ ): Promise<{ newCredentials: OAuthCredentials; apiKey: string } | null> {
97
+ let creds = credentials[provider];
98
+ if (!creds) {
99
+ return null;
100
+ }
101
+
102
+ // Refresh if expired
103
+ if (Date.now() >= creds.expires) {
104
+ try {
105
+ creds = await refreshOAuthToken(provider, creds);
106
+ } catch (_error) {
107
+ throw new Error(`Failed to refresh OAuth token for ${provider}`);
108
+ }
109
+ }
110
+
111
+ // For providers that need projectId, return JSON
112
+ const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity";
113
+ const apiKey = needsProjectId ? JSON.stringify({ token: creds.access, projectId: creds.projectId }) : creds.access;
114
+ return { newCredentials: creds, apiKey };
115
+ }
116
+
117
+ /**
118
+ * Get list of OAuth providers
119
+ */
120
+ export function getOAuthProviders(): OAuthProviderInfo[] {
121
+ return [
122
+ {
123
+ id: "anthropic",
124
+ name: "Anthropic (Claude Pro/Max)",
125
+ available: true,
126
+ },
127
+ {
128
+ id: "github-copilot",
129
+ name: "GitHub Copilot",
130
+ available: true,
131
+ },
132
+ {
133
+ id: "google-gemini-cli",
134
+ name: "Google Cloud Code Assist (Gemini CLI)",
135
+ available: true,
136
+ },
137
+ {
138
+ id: "google-antigravity",
139
+ name: "Antigravity (Gemini 3, Claude, GPT-OSS)",
140
+ available: true,
141
+ },
142
+ ];
143
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * PKCE utilities using Web Crypto API.
3
+ * Works in both Node.js 20+ and browsers.
4
+ */
5
+
6
+ /**
7
+ * Encode bytes as base64url string.
8
+ */
9
+ function base64urlEncode(bytes: Uint8Array): string {
10
+ let binary = "";
11
+ for (const byte of bytes) {
12
+ binary += String.fromCharCode(byte);
13
+ }
14
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
15
+ }
16
+
17
+ /**
18
+ * Generate PKCE code verifier and challenge.
19
+ * Uses Web Crypto API for cross-platform compatibility.
20
+ */
21
+ export async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
22
+ // Generate random verifier
23
+ const verifierBytes = new Uint8Array(32);
24
+ crypto.getRandomValues(verifierBytes);
25
+ const verifier = base64urlEncode(verifierBytes);
26
+
27
+ // Compute SHA-256 challenge
28
+ const encoder = new TextEncoder();
29
+ const data = encoder.encode(verifier);
30
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
31
+ const challenge = base64urlEncode(new Uint8Array(hashBuffer));
32
+
33
+ return { verifier, challenge };
34
+ }
@@ -0,0 +1,27 @@
1
+ export type OAuthCredentials = {
2
+ refresh: string;
3
+ access: string;
4
+ expires: number;
5
+ enterpriseUrl?: string;
6
+ projectId?: string;
7
+ email?: string;
8
+ };
9
+
10
+ export type OAuthProvider = "anthropic" | "github-copilot" | "google-gemini-cli" | "google-antigravity";
11
+
12
+ export type OAuthPrompt = {
13
+ message: string;
14
+ placeholder?: string;
15
+ allowEmpty?: boolean;
16
+ };
17
+
18
+ export type OAuthAuthInfo = {
19
+ url: string;
20
+ instructions?: string;
21
+ };
22
+
23
+ export interface OAuthProviderInfo {
24
+ id: OAuthProvider;
25
+ name: string;
26
+ available: boolean;
27
+ }
@@ -0,0 +1,115 @@
1
+ import type { AssistantMessage } from "../types.js";
2
+
3
+ /**
4
+ * Regex patterns to detect context overflow errors from different providers.
5
+ *
6
+ * These patterns match error messages returned when the input exceeds
7
+ * the model's context window.
8
+ *
9
+ * Provider-specific patterns (with example error messages):
10
+ *
11
+ * - Anthropic: "prompt is too long: 213462 tokens > 200000 maximum"
12
+ * - OpenAI: "Your input exceeds the context window of this model"
13
+ * - Google: "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)"
14
+ * - xAI: "This model's maximum prompt length is 131072 but the request contains 537812 tokens"
15
+ * - Groq: "Please reduce the length of the messages or completion"
16
+ * - OpenRouter: "This endpoint's maximum context length is X tokens. However, you requested about Y tokens"
17
+ * - llama.cpp: "the request exceeds the available context size, try increasing it"
18
+ * - LM Studio: "tokens to keep from the initial prompt is greater than the context length"
19
+ * - GitHub Copilot: "prompt token count of X exceeds the limit of Y"
20
+ * - Cerebras: Returns "400 status code (no body)" - handled separately below
21
+ * - Mistral: Returns "400 status code (no body)" - handled separately below
22
+ * - z.ai: Does NOT error, accepts overflow silently - handled via usage.input > contextWindow
23
+ * - Ollama: Silently truncates input - not detectable via error message
24
+ */
25
+ const OVERFLOW_PATTERNS = [
26
+ /prompt is too long/i, // Anthropic
27
+ /exceeds the context window/i, // OpenAI (Completions & Responses API)
28
+ /input token count.*exceeds the maximum/i, // Google (Gemini)
29
+ /maximum prompt length is \d+/i, // xAI (Grok)
30
+ /reduce the length of the messages/i, // Groq
31
+ /maximum context length is \d+ tokens/i, // OpenRouter (all backends)
32
+ /exceeds the limit of \d+/i, // GitHub Copilot
33
+ /exceeds the available context size/i, // llama.cpp server
34
+ /greater than the context length/i, // LM Studio
35
+ /context length exceeded/i, // Generic fallback
36
+ /too many tokens/i, // Generic fallback
37
+ /token limit exceeded/i, // Generic fallback
38
+ ];
39
+
40
+ /**
41
+ * Check if an assistant message represents a context overflow error.
42
+ *
43
+ * This handles two cases:
44
+ * 1. Error-based overflow: Most providers return stopReason "error" with a
45
+ * specific error message pattern.
46
+ * 2. Silent overflow: Some providers accept overflow requests and return
47
+ * successfully. For these, we check if usage.input exceeds the context window.
48
+ *
49
+ * ## Reliability by Provider
50
+ *
51
+ * **Reliable detection (returns error with detectable message):**
52
+ * - Anthropic: "prompt is too long: X tokens > Y maximum"
53
+ * - OpenAI (Completions & Responses): "exceeds the context window"
54
+ * - Google Gemini: "input token count exceeds the maximum"
55
+ * - xAI (Grok): "maximum prompt length is X but request contains Y"
56
+ * - Groq: "reduce the length of the messages"
57
+ * - Cerebras: 400/413/429 status code (no body)
58
+ * - Mistral: 400/413/429 status code (no body)
59
+ * - OpenRouter (all backends): "maximum context length is X tokens"
60
+ * - llama.cpp: "exceeds the available context size"
61
+ * - LM Studio: "greater than the context length"
62
+ *
63
+ * **Unreliable detection:**
64
+ * - z.ai: Sometimes accepts overflow silently (detectable via usage.input > contextWindow),
65
+ * sometimes returns rate limit errors. Pass contextWindow param to detect silent overflow.
66
+ * - Ollama: Silently truncates input without error. Cannot be detected via this function.
67
+ * The response will have usage.input < expected, but we don't know the expected value.
68
+ *
69
+ * ## Custom Providers
70
+ *
71
+ * If you've added custom models via settings.json, this function may not detect
72
+ * overflow errors from those providers. To add support:
73
+ *
74
+ * 1. Send a request that exceeds the model's context window
75
+ * 2. Check the errorMessage in the response
76
+ * 3. Create a regex pattern that matches the error
77
+ * 4. The pattern should be added to OVERFLOW_PATTERNS in this file, or
78
+ * check the errorMessage yourself before calling this function
79
+ *
80
+ * @param message - The assistant message to check
81
+ * @param contextWindow - Optional context window size for detecting silent overflow (z.ai)
82
+ * @returns true if the message indicates a context overflow
83
+ */
84
+ export function isContextOverflow(message: AssistantMessage, contextWindow?: number): boolean {
85
+ // Case 1: Check error message patterns
86
+ if (message.stopReason === "error" && message.errorMessage) {
87
+ // Check known patterns
88
+ if (OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage!))) {
89
+ return true;
90
+ }
91
+
92
+ // Cerebras and Mistral return 400/413/429 with no body - check for status code pattern
93
+ // 429 can indicate token-based rate limiting which correlates with context overflow
94
+ if (/^4(00|13|29)\s*(status code)?\s*\(no body\)/i.test(message.errorMessage)) {
95
+ return true;
96
+ }
97
+ }
98
+
99
+ // Case 2: Silent overflow (z.ai style) - successful but usage exceeds context
100
+ if (contextWindow && message.stopReason === "stop") {
101
+ const inputTokens = message.usage.input + message.usage.cacheRead;
102
+ if (inputTokens > contextWindow) {
103
+ return true;
104
+ }
105
+ }
106
+
107
+ return false;
108
+ }
109
+
110
+ /**
111
+ * Get the overflow patterns for testing purposes.
112
+ */
113
+ export function getOverflowPatterns(): RegExp[] {
114
+ return [...OVERFLOW_PATTERNS];
115
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Removes unpaired Unicode surrogate characters from a string.
3
+ *
4
+ * Unpaired surrogates (high surrogates 0xD800-0xDBFF without matching low surrogates 0xDC00-0xDFFF,
5
+ * or vice versa) cause JSON serialization errors in many API providers.
6
+ *
7
+ * Valid emoji and other characters outside the Basic Multilingual Plane use properly paired
8
+ * surrogates and will NOT be affected by this function.
9
+ *
10
+ * @param text - The text to sanitize
11
+ * @returns The sanitized text with unpaired surrogates removed
12
+ *
13
+ * @example
14
+ * // Valid emoji (properly paired surrogates) are preserved
15
+ * sanitizeSurrogates("Hello 🙈 World") // => "Hello 🙈 World"
16
+ *
17
+ * // Unpaired high surrogate is removed
18
+ * const unpaired = String.fromCharCode(0xD83D); // high surrogate without low
19
+ * sanitizeSurrogates(`Text ${unpaired} here`) // => "Text here"
20
+ */
21
+ export function sanitizeSurrogates(text: string): string {
22
+ // Replace unpaired high surrogates (0xD800-0xDBFF not followed by low surrogate)
23
+ // Replace unpaired low surrogates (0xDC00-0xDFFF not preceded by high surrogate)
24
+ return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
25
+ }
@@ -0,0 +1,24 @@
1
+ import { type TUnsafe, Type } from "@sinclair/typebox";
2
+
3
+ /**
4
+ * Creates a string enum schema compatible with Google's API and other providers
5
+ * that don't support anyOf/const patterns.
6
+ *
7
+ * @example
8
+ * const OperationSchema = StringEnum(["add", "subtract", "multiply", "divide"], {
9
+ * description: "The operation to perform"
10
+ * });
11
+ *
12
+ * type Operation = Static<typeof OperationSchema>; // "add" | "subtract" | "multiply" | "divide"
13
+ */
14
+ export function StringEnum<T extends readonly string[]>(
15
+ values: T,
16
+ options?: { description?: string; default?: T[number] },
17
+ ): TUnsafe<T[number]> {
18
+ return Type.Unsafe<T[number]>({
19
+ type: "string",
20
+ enum: values as any,
21
+ ...(options?.description && { description: options.description }),
22
+ ...(options?.default && { default: options.default }),
23
+ });
24
+ }
@@ -0,0 +1,80 @@
1
+ import AjvModule from "ajv";
2
+ import addFormatsModule from "ajv-formats";
3
+
4
+ // Handle both default and named exports
5
+ const Ajv = (AjvModule as any).default || AjvModule;
6
+ const addFormats = (addFormatsModule as any).default || addFormatsModule;
7
+
8
+ import type { Tool, ToolCall } from "../types.js";
9
+
10
+ // Detect if we're in a browser extension environment with strict CSP
11
+ // Chrome extensions with Manifest V3 don't allow eval/Function constructor
12
+ const isBrowserExtension = typeof globalThis !== "undefined" && (globalThis as any).chrome?.runtime?.id !== undefined;
13
+
14
+ // Create a singleton AJV instance with formats (only if not in browser extension)
15
+ // AJV requires 'unsafe-eval' CSP which is not allowed in Manifest V3
16
+ let ajv: any = null;
17
+ if (!isBrowserExtension) {
18
+ try {
19
+ ajv = new Ajv({
20
+ allErrors: true,
21
+ strict: false,
22
+ });
23
+ addFormats(ajv);
24
+ } catch (_e) {
25
+ // AJV initialization failed (likely CSP restriction)
26
+ console.warn("AJV validation disabled due to CSP restrictions");
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Finds a tool by name and validates the tool call arguments against its TypeBox schema
32
+ * @param tools Array of tool definitions
33
+ * @param toolCall The tool call from the LLM
34
+ * @returns The validated arguments
35
+ * @throws Error if tool is not found or validation fails
36
+ */
37
+ export function validateToolCall(tools: Tool[], toolCall: ToolCall): any {
38
+ const tool = tools.find((t) => t.name === toolCall.name);
39
+ if (!tool) {
40
+ throw new Error(`Tool "${toolCall.name}" not found`);
41
+ }
42
+ return validateToolArguments(tool, toolCall);
43
+ }
44
+
45
+ /**
46
+ * Validates tool call arguments against the tool's TypeBox schema
47
+ * @param tool The tool definition with TypeBox schema
48
+ * @param toolCall The tool call from the LLM
49
+ * @returns The validated arguments
50
+ * @throws Error with formatted message if validation fails
51
+ */
52
+ export function validateToolArguments(tool: Tool, toolCall: ToolCall): any {
53
+ // Skip validation in browser extension environment (CSP restrictions prevent AJV from working)
54
+ if (!ajv || isBrowserExtension) {
55
+ // Trust the LLM's output without validation
56
+ // Browser extensions can't use AJV due to Manifest V3 CSP restrictions
57
+ return toolCall.arguments;
58
+ }
59
+
60
+ // Compile the schema
61
+ const validate = ajv.compile(tool.parameters);
62
+
63
+ // Validate the arguments
64
+ if (validate(toolCall.arguments)) {
65
+ return toolCall.arguments;
66
+ }
67
+
68
+ // Format validation errors nicely
69
+ const errors =
70
+ validate.errors
71
+ ?.map((err: any) => {
72
+ const path = err.instancePath ? err.instancePath.substring(1) : err.params.missingProperty || "root";
73
+ return ` - ${path}: ${err.message}`;
74
+ })
75
+ .join("\n") || "Unknown validation error";
76
+
77
+ const errorMessage = `Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}`;
78
+
79
+ throw new Error(errorMessage);
80
+ }