@indykish/oracle 0.9.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.
Files changed (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +1252 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/scripts/agent-send.js +147 -0
  7. package/dist/scripts/browser-tools.js +536 -0
  8. package/dist/scripts/check.js +21 -0
  9. package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
  10. package/dist/scripts/docs-list.js +110 -0
  11. package/dist/scripts/git-policy.js +125 -0
  12. package/dist/scripts/run-cli.js +14 -0
  13. package/dist/scripts/runner.js +1378 -0
  14. package/dist/scripts/test-browser.js +103 -0
  15. package/dist/scripts/test-remote-chrome.js +68 -0
  16. package/dist/src/bridge/connection.js +103 -0
  17. package/dist/src/bridge/userConfigFile.js +28 -0
  18. package/dist/src/browser/actions/assistantResponse.js +1067 -0
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
  20. package/dist/src/browser/actions/attachments.js +1910 -0
  21. package/dist/src/browser/actions/domEvents.js +19 -0
  22. package/dist/src/browser/actions/modelSelection.js +485 -0
  23. package/dist/src/browser/actions/navigation.js +445 -0
  24. package/dist/src/browser/actions/promptComposer.js +485 -0
  25. package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
  26. package/dist/src/browser/actions/thinkingTime.js +206 -0
  27. package/dist/src/browser/chromeLifecycle.js +344 -0
  28. package/dist/src/browser/config.js +103 -0
  29. package/dist/src/browser/constants.js +71 -0
  30. package/dist/src/browser/cookies.js +191 -0
  31. package/dist/src/browser/detect.js +164 -0
  32. package/dist/src/browser/domDebug.js +36 -0
  33. package/dist/src/browser/index.js +1741 -0
  34. package/dist/src/browser/modelStrategy.js +13 -0
  35. package/dist/src/browser/pageActions.js +5 -0
  36. package/dist/src/browser/policies.js +43 -0
  37. package/dist/src/browser/profileState.js +280 -0
  38. package/dist/src/browser/prompt.js +152 -0
  39. package/dist/src/browser/promptSummary.js +20 -0
  40. package/dist/src/browser/reattach.js +186 -0
  41. package/dist/src/browser/reattachHelpers.js +382 -0
  42. package/dist/src/browser/sessionRunner.js +119 -0
  43. package/dist/src/browser/types.js +1 -0
  44. package/dist/src/browser/utils.js +122 -0
  45. package/dist/src/browserMode.js +1 -0
  46. package/dist/src/cli/bridge/claudeConfig.js +54 -0
  47. package/dist/src/cli/bridge/client.js +73 -0
  48. package/dist/src/cli/bridge/codexConfig.js +43 -0
  49. package/dist/src/cli/bridge/doctor.js +107 -0
  50. package/dist/src/cli/bridge/host.js +259 -0
  51. package/dist/src/cli/browserConfig.js +278 -0
  52. package/dist/src/cli/browserDefaults.js +81 -0
  53. package/dist/src/cli/bundleWarnings.js +9 -0
  54. package/dist/src/cli/clipboard.js +10 -0
  55. package/dist/src/cli/detach.js +11 -0
  56. package/dist/src/cli/dryRun.js +105 -0
  57. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  58. package/dist/src/cli/engine.js +41 -0
  59. package/dist/src/cli/errorUtils.js +9 -0
  60. package/dist/src/cli/format.js +13 -0
  61. package/dist/src/cli/help.js +77 -0
  62. package/dist/src/cli/hiddenAliases.js +22 -0
  63. package/dist/src/cli/markdownBundle.js +17 -0
  64. package/dist/src/cli/markdownRenderer.js +97 -0
  65. package/dist/src/cli/notifier.js +306 -0
  66. package/dist/src/cli/options.js +281 -0
  67. package/dist/src/cli/oscUtils.js +2 -0
  68. package/dist/src/cli/promptRequirement.js +17 -0
  69. package/dist/src/cli/renderFlags.js +9 -0
  70. package/dist/src/cli/renderOutput.js +26 -0
  71. package/dist/src/cli/rootAlias.js +30 -0
  72. package/dist/src/cli/runOptions.js +78 -0
  73. package/dist/src/cli/sessionCommand.js +111 -0
  74. package/dist/src/cli/sessionDisplay.js +567 -0
  75. package/dist/src/cli/sessionRunner.js +602 -0
  76. package/dist/src/cli/sessionTable.js +92 -0
  77. package/dist/src/cli/tagline.js +258 -0
  78. package/dist/src/cli/tui/index.js +486 -0
  79. package/dist/src/cli/writeOutputPath.js +21 -0
  80. package/dist/src/config.js +26 -0
  81. package/dist/src/gemini-web/client.js +328 -0
  82. package/dist/src/gemini-web/executor.js +285 -0
  83. package/dist/src/gemini-web/index.js +1 -0
  84. package/dist/src/gemini-web/types.js +1 -0
  85. package/dist/src/heartbeat.js +43 -0
  86. package/dist/src/mcp/server.js +40 -0
  87. package/dist/src/mcp/tools/consult.js +290 -0
  88. package/dist/src/mcp/tools/sessionResources.js +75 -0
  89. package/dist/src/mcp/tools/sessions.js +105 -0
  90. package/dist/src/mcp/types.js +22 -0
  91. package/dist/src/mcp/utils.js +37 -0
  92. package/dist/src/oracle/background.js +141 -0
  93. package/dist/src/oracle/claude.js +101 -0
  94. package/dist/src/oracle/client.js +197 -0
  95. package/dist/src/oracle/config.js +227 -0
  96. package/dist/src/oracle/errors.js +132 -0
  97. package/dist/src/oracle/files.js +378 -0
  98. package/dist/src/oracle/finishLine.js +32 -0
  99. package/dist/src/oracle/format.js +30 -0
  100. package/dist/src/oracle/fsAdapter.js +10 -0
  101. package/dist/src/oracle/gemini.js +195 -0
  102. package/dist/src/oracle/logging.js +36 -0
  103. package/dist/src/oracle/markdown.js +46 -0
  104. package/dist/src/oracle/modelResolver.js +183 -0
  105. package/dist/src/oracle/multiModelRunner.js +153 -0
  106. package/dist/src/oracle/oscProgress.js +24 -0
  107. package/dist/src/oracle/promptAssembly.js +13 -0
  108. package/dist/src/oracle/request.js +50 -0
  109. package/dist/src/oracle/run.js +596 -0
  110. package/dist/src/oracle/runUtils.js +31 -0
  111. package/dist/src/oracle/tokenEstimate.js +37 -0
  112. package/dist/src/oracle/tokenStats.js +39 -0
  113. package/dist/src/oracle/tokenStringifier.js +24 -0
  114. package/dist/src/oracle/types.js +1 -0
  115. package/dist/src/oracle.js +12 -0
  116. package/dist/src/oracleHome.js +13 -0
  117. package/dist/src/remote/client.js +129 -0
  118. package/dist/src/remote/health.js +113 -0
  119. package/dist/src/remote/remoteServiceConfig.js +31 -0
  120. package/dist/src/remote/server.js +533 -0
  121. package/dist/src/remote/types.js +1 -0
  122. package/dist/src/sessionManager.js +637 -0
  123. package/dist/src/sessionStore.js +56 -0
  124. package/dist/src/version.js +39 -0
  125. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  126. package/dist/vendor/oracle-notifier/README.md +24 -0
  127. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  128. package/package.json +115 -0
  129. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  130. package/vendor/oracle-notifier/README.md +24 -0
  131. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,141 @@
1
+ import { APIConnectionError, APIConnectionTimeoutError } from 'openai';
2
+ import chalk from 'chalk';
3
+ import { formatElapsed } from './format.js';
4
+ import { startHeartbeat } from '../heartbeat.js';
5
+ import { OracleResponseError, OracleTransportError, describeTransportError, toTransportError, } from './errors.js';
6
+ const BACKGROUND_POLL_INTERVAL_MS = 5000;
7
+ const BACKGROUND_RETRY_BASE_MS = 3000;
8
+ const BACKGROUND_RETRY_MAX_MS = 15000;
9
+ export async function executeBackgroundResponse(params) {
10
+ const { client, requestBody, log, wait, heartbeatIntervalMs, now, maxWaitMs } = params;
11
+ let initialResponse;
12
+ try {
13
+ initialResponse = await client.responses.create(requestBody);
14
+ }
15
+ catch (error) {
16
+ const transportError = toTransportError(error, requestBody.model);
17
+ log(chalk.yellow(describeTransportError(transportError, maxWaitMs)));
18
+ throw transportError;
19
+ }
20
+ if (!initialResponse || !initialResponse.id) {
21
+ throw new OracleResponseError('API did not return a response ID for the background run.', initialResponse);
22
+ }
23
+ const responseId = initialResponse.id;
24
+ log(chalk.dim(`API scheduled background response ${responseId} (status=${initialResponse.status ?? 'unknown'}). Monitoring up to ${Math.round(maxWaitMs / 60000)} minutes for completion...`));
25
+ let heartbeatActive = false;
26
+ let stopHeartbeat = null;
27
+ const stopHeartbeatNow = () => {
28
+ if (!heartbeatActive)
29
+ return;
30
+ heartbeatActive = false;
31
+ stopHeartbeat?.();
32
+ stopHeartbeat = null;
33
+ };
34
+ if (heartbeatIntervalMs && heartbeatIntervalMs > 0) {
35
+ heartbeatActive = true;
36
+ stopHeartbeat = startHeartbeat({
37
+ intervalMs: heartbeatIntervalMs,
38
+ log: (message) => log(message),
39
+ isActive: () => heartbeatActive,
40
+ makeMessage: (elapsedMs) => {
41
+ const elapsedText = formatElapsed(elapsedMs);
42
+ return `API background run still in progress — ${elapsedText} elapsed.`;
43
+ },
44
+ });
45
+ }
46
+ try {
47
+ return await pollBackgroundResponse({
48
+ client,
49
+ responseId,
50
+ initialResponse,
51
+ log,
52
+ wait,
53
+ now,
54
+ maxWaitMs,
55
+ });
56
+ }
57
+ finally {
58
+ stopHeartbeatNow();
59
+ }
60
+ }
61
+ async function pollBackgroundResponse(params) {
62
+ const { client, responseId, initialResponse, log, wait, now, maxWaitMs } = params;
63
+ const startMark = now();
64
+ let response = initialResponse;
65
+ let firstCycle = true;
66
+ let lastStatus = response.status;
67
+ // biome-ignore lint/nursery/noUnnecessaryConditions: intentional polling loop.
68
+ while (true) {
69
+ const status = response.status ?? 'completed';
70
+ // firstCycle toggles immediately; keep for clarity in logs.
71
+ if (firstCycle) {
72
+ firstCycle = false;
73
+ log(chalk.dim(`API background response status=${status}. We'll keep retrying automatically.`));
74
+ }
75
+ else if (status !== lastStatus && status !== 'completed') {
76
+ log(chalk.dim(`API background response status=${status}.`));
77
+ }
78
+ lastStatus = status;
79
+ if (status === 'completed') {
80
+ return response;
81
+ }
82
+ if (status !== 'in_progress' && status !== 'queued') {
83
+ const detail = response.error?.message || response.incomplete_details?.reason || status;
84
+ throw new OracleResponseError(`Response did not complete: ${detail}`, response);
85
+ }
86
+ if (now() - startMark >= maxWaitMs) {
87
+ throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
88
+ }
89
+ await wait(BACKGROUND_POLL_INTERVAL_MS);
90
+ if (now() - startMark >= maxWaitMs) {
91
+ throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
92
+ }
93
+ const { response: nextResponse, reconnected } = await retrieveBackgroundResponseWithRetry({
94
+ client,
95
+ responseId,
96
+ wait,
97
+ now,
98
+ maxWaitMs,
99
+ startMark,
100
+ log,
101
+ });
102
+ if (reconnected) {
103
+ const nextStatus = nextResponse.status ?? 'in_progress';
104
+ log(chalk.dim(`Reconnected to API background response (status=${nextStatus}). API is still working...`));
105
+ }
106
+ response = nextResponse;
107
+ }
108
+ }
109
+ async function retrieveBackgroundResponseWithRetry(params) {
110
+ const { client, responseId, wait, now, maxWaitMs, startMark, log } = params;
111
+ let retries = 0;
112
+ // biome-ignore lint/nursery/noUnnecessaryConditions: intentional retry loop
113
+ while (true) {
114
+ try {
115
+ const next = await client.responses.retrieve(responseId);
116
+ return { response: next, reconnected: retries > 0 };
117
+ }
118
+ catch (error) {
119
+ const transportError = asRetryableTransportError(error);
120
+ if (!transportError) {
121
+ throw error;
122
+ }
123
+ retries += 1;
124
+ const delay = Math.min(BACKGROUND_RETRY_BASE_MS * 2 ** (retries - 1), BACKGROUND_RETRY_MAX_MS);
125
+ log(chalk.yellow(`${describeTransportError(transportError, maxWaitMs)} Retrying in ${formatElapsed(delay)}...`));
126
+ await wait(delay);
127
+ if (now() - startMark >= maxWaitMs) {
128
+ throw new OracleTransportError('client-timeout', 'Timed out waiting for API background response to finish.');
129
+ }
130
+ }
131
+ }
132
+ }
133
+ function asRetryableTransportError(error) {
134
+ if (error instanceof OracleTransportError) {
135
+ return error;
136
+ }
137
+ if (error instanceof APIConnectionError || error instanceof APIConnectionTimeoutError) {
138
+ return toTransportError(error);
139
+ }
140
+ return null;
141
+ }
@@ -0,0 +1,101 @@
1
+ const DEFAULT_CLAUDE_ENDPOINT = 'https://api.anthropic.com/v1/messages';
2
+ const ANTHROPIC_VERSION = '2023-06-01';
3
+ function extractPrompt(body) {
4
+ const first = body.input?.[0]?.content?.[0];
5
+ if (first && first.type === 'input_text') {
6
+ return first.text ?? '';
7
+ }
8
+ return '';
9
+ }
10
+ async function callClaude({ apiKey, model, prompt, endpoint, stream = false, }) {
11
+ const url = endpoint?.trim() || DEFAULT_CLAUDE_ENDPOINT;
12
+ const payload = {
13
+ model,
14
+ max_tokens: 2048,
15
+ messages: [
16
+ {
17
+ role: 'user',
18
+ content: prompt,
19
+ },
20
+ ],
21
+ stream,
22
+ };
23
+ return fetch(url, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'content-type': 'application/json',
27
+ 'x-api-key': apiKey,
28
+ 'anthropic-version': ANTHROPIC_VERSION,
29
+ },
30
+ body: JSON.stringify(payload),
31
+ });
32
+ }
33
+ async function parseClaudeResponse(raw) {
34
+ const json = (await raw.json());
35
+ if (json.error) {
36
+ throw new Error(json.error.message || 'Claude request failed');
37
+ }
38
+ const textParts = json.content?.map((part) => part.text ?? '').filter(Boolean) ?? [];
39
+ const outputText = textParts.join('');
40
+ return {
41
+ id: json.id ?? `claude-${Date.now()}`,
42
+ status: 'completed',
43
+ output_text: [outputText],
44
+ output: [{ type: 'text', text: outputText }],
45
+ usage: {
46
+ input_tokens: json.usage?.input_tokens ?? 0,
47
+ output_tokens: json.usage?.output_tokens ?? 0,
48
+ total_tokens: (json.usage?.input_tokens ?? 0) + (json.usage?.output_tokens ?? 0),
49
+ },
50
+ };
51
+ }
52
+ export function createClaudeClient(apiKey, modelName, resolvedModelId, baseUrl) {
53
+ const modelId = resolveClaudeModelId(resolvedModelId ?? modelName);
54
+ const stream = async (body) => {
55
+ const prompt = extractPrompt(body);
56
+ const resp = await callClaude({ apiKey, model: modelId, prompt, stream: false, endpoint: baseUrl });
57
+ const parsed = await parseClaudeResponse(resp);
58
+ const iterator = async function* () {
59
+ if (parsed.output_text?.[0]) {
60
+ yield { type: 'response.output_text.delta', delta: parsed.output_text[0] };
61
+ }
62
+ return;
63
+ };
64
+ return {
65
+ [Symbol.asyncIterator]: () => iterator(),
66
+ finalResponse: async () => parsed,
67
+ };
68
+ };
69
+ const create = async (body) => {
70
+ const prompt = extractPrompt(body);
71
+ const resp = await callClaude({ apiKey, model: modelId, prompt, stream: false, endpoint: baseUrl });
72
+ return parseClaudeResponse(resp);
73
+ };
74
+ const retrieve = async (id) => ({
75
+ id,
76
+ status: 'error',
77
+ error: { message: 'Retrieve by ID not supported for Claude API yet.' },
78
+ });
79
+ return {
80
+ responses: {
81
+ stream,
82
+ create,
83
+ retrieve,
84
+ },
85
+ };
86
+ }
87
+ export function resolveClaudeModelId(modelName) {
88
+ if (modelName === 'claude-4.6-sonnet') {
89
+ return 'claude-sonnet-4-6';
90
+ }
91
+ if (modelName === 'claude-4.6-opus') {
92
+ return 'claude-opus-4-6';
93
+ }
94
+ if (modelName === 'claude-4.5-sonnet' || modelName === 'claude-sonnet-4-5-20241022') {
95
+ return 'claude-sonnet-4-5';
96
+ }
97
+ if (modelName === 'claude-4.1-opus' || modelName === 'claude-opus-4-1-20240808') {
98
+ return 'claude-opus-4-1';
99
+ }
100
+ return modelName;
101
+ }
@@ -0,0 +1,197 @@
1
+ import OpenAI, { AzureOpenAI } from 'openai';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { createGeminiClient } from './gemini.js';
5
+ import { createClaudeClient } from './claude.js';
6
+ import { isOpenRouterBaseUrl } from './modelResolver.js';
7
+ export function createDefaultClientFactory() {
8
+ const customFactory = loadCustomClientFactory();
9
+ if (customFactory)
10
+ return customFactory;
11
+ return (key, options) => {
12
+ if (options?.model?.startsWith('gemini')) {
13
+ // Gemini client uses its own SDK; allow passing the already-resolved id for transparency/logging.
14
+ return createGeminiClient(key, options.model, options.resolvedModelId);
15
+ }
16
+ if (options?.model?.startsWith('claude')) {
17
+ return createClaudeClient(key, options.model, options.resolvedModelId, options.baseUrl);
18
+ }
19
+ let instance;
20
+ const openRouter = isOpenRouterBaseUrl(options?.baseUrl);
21
+ const defaultHeaders = openRouter ? buildOpenRouterHeaders() : undefined;
22
+ const httpTimeoutMs = typeof options?.httpTimeoutMs === 'number' && Number.isFinite(options.httpTimeoutMs) && options.httpTimeoutMs > 0
23
+ ? options.httpTimeoutMs
24
+ : 20 * 60 * 1000;
25
+ if (options?.azure?.endpoint) {
26
+ instance = new AzureOpenAI({
27
+ apiKey: key,
28
+ endpoint: options.azure.endpoint,
29
+ apiVersion: options.azure.apiVersion,
30
+ deployment: options.azure.deployment,
31
+ timeout: httpTimeoutMs,
32
+ });
33
+ }
34
+ else {
35
+ instance = new OpenAI({
36
+ apiKey: key,
37
+ timeout: httpTimeoutMs,
38
+ baseURL: options?.baseUrl,
39
+ defaultHeaders,
40
+ });
41
+ }
42
+ if (openRouter) {
43
+ return buildOpenRouterCompletionClient(instance);
44
+ }
45
+ return {
46
+ responses: {
47
+ stream: (body) => instance.responses.stream(body),
48
+ create: (body) => instance.responses.create(body),
49
+ retrieve: (id) => instance.responses.retrieve(id),
50
+ },
51
+ };
52
+ };
53
+ }
54
+ function buildOpenRouterHeaders() {
55
+ const headers = {};
56
+ const referer = process.env.OPENROUTER_REFERER ?? process.env.OPENROUTER_HTTP_REFERER ?? 'https://github.com/steipete/oracle';
57
+ const title = process.env.OPENROUTER_TITLE ?? 'Oracle CLI';
58
+ if (referer) {
59
+ headers['HTTP-Referer'] = referer;
60
+ }
61
+ if (title) {
62
+ headers['X-Title'] = title;
63
+ }
64
+ return headers;
65
+ }
66
+ function loadCustomClientFactory() {
67
+ const override = process.env.ORACLE_CLIENT_FACTORY;
68
+ if (!override) {
69
+ return null;
70
+ }
71
+ if (override === 'INLINE_TEST_FACTORY') {
72
+ return () => ({
73
+ responses: {
74
+ create: async () => ({ id: 'inline-test', status: 'completed' }),
75
+ stream: async () => ({
76
+ [Symbol.asyncIterator]: () => ({
77
+ async next() {
78
+ return { done: true, value: undefined };
79
+ },
80
+ }),
81
+ finalResponse: async () => ({ id: 'inline-test', status: 'completed' }),
82
+ }),
83
+ retrieve: async (id) => ({ id, status: 'completed' }),
84
+ },
85
+ });
86
+ }
87
+ try {
88
+ const require = createRequire(import.meta.url);
89
+ const resolved = path.isAbsolute(override) ? override : path.resolve(process.cwd(), override);
90
+ const moduleExports = require(resolved);
91
+ const factory = typeof moduleExports === 'function'
92
+ ? moduleExports
93
+ : typeof moduleExports?.default === 'function'
94
+ ? moduleExports.default
95
+ : typeof moduleExports?.createClientFactory === 'function'
96
+ ? moduleExports.createClientFactory
97
+ : null;
98
+ if (typeof factory === 'function') {
99
+ return factory;
100
+ }
101
+ console.warn(`Custom client factory at ${resolved} did not export a function.`);
102
+ }
103
+ catch (error) {
104
+ console.warn(`Failed to load ORACLE_CLIENT_FACTORY module "${override}":`, error);
105
+ }
106
+ return null;
107
+ }
108
+ // Exposed for tests
109
+ export { loadCustomClientFactory as __loadCustomClientFactory };
110
+ function buildOpenRouterCompletionClient(instance) {
111
+ const adaptRequest = (body) => {
112
+ const messages = [];
113
+ if (body.instructions) {
114
+ messages.push({ role: 'system', content: body.instructions });
115
+ }
116
+ for (const entry of body.input) {
117
+ const textParts = entry.content
118
+ .map((c) => (c.type === 'input_text' ? c.text : ''))
119
+ .filter((t) => t)
120
+ .join('\n\n');
121
+ messages.push({ role: entry.role ?? 'user', content: textParts });
122
+ }
123
+ const base = {
124
+ model: body.model,
125
+ messages,
126
+ max_tokens: body.max_output_tokens,
127
+ };
128
+ const streaming = { ...base, stream: true };
129
+ const nonStreaming = { ...base, stream: false };
130
+ return { streaming, nonStreaming };
131
+ };
132
+ const adaptResponse = (response) => {
133
+ const text = response.choices?.[0]?.message?.content ?? '';
134
+ const usage = {
135
+ input_tokens: response.usage?.prompt_tokens ?? 0,
136
+ output_tokens: response.usage?.completion_tokens ?? 0,
137
+ total_tokens: response.usage?.total_tokens ?? 0,
138
+ };
139
+ return {
140
+ id: response.id ?? `openrouter-${Date.now()}`,
141
+ status: 'completed',
142
+ output_text: [text],
143
+ output: [{ type: 'text', text }],
144
+ usage,
145
+ };
146
+ };
147
+ const stream = async (body) => {
148
+ const { streaming } = adaptRequest(body);
149
+ let finalUsage;
150
+ let finalId;
151
+ let aggregated = '';
152
+ async function* iterator() {
153
+ const completion = await instance.chat.completions.create(streaming);
154
+ for await (const chunk of completion) {
155
+ finalId = chunk.id ?? finalId;
156
+ const delta = chunk.choices?.[0]?.delta?.content ?? '';
157
+ if (delta) {
158
+ aggregated += delta;
159
+ yield { type: 'chunk', delta };
160
+ }
161
+ if (chunk.usage) {
162
+ finalUsage = chunk.usage;
163
+ }
164
+ }
165
+ }
166
+ const gen = iterator();
167
+ return {
168
+ [Symbol.asyncIterator]() {
169
+ return gen;
170
+ },
171
+ async finalResponse() {
172
+ return adaptResponse({
173
+ id: finalId ?? `openrouter-${Date.now()}`,
174
+ choices: [{ message: { role: 'assistant', content: aggregated } }],
175
+ usage: finalUsage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
176
+ created: Math.floor(Date.now() / 1000),
177
+ model: '',
178
+ object: 'chat.completion',
179
+ });
180
+ },
181
+ };
182
+ };
183
+ const create = async (body) => {
184
+ const { nonStreaming } = adaptRequest(body);
185
+ const response = (await instance.chat.completions.create(nonStreaming));
186
+ return adaptResponse(response);
187
+ };
188
+ return {
189
+ responses: {
190
+ stream,
191
+ create,
192
+ retrieve: async () => {
193
+ throw new Error('retrieve is not supported for OpenRouter chat/completions fallback.');
194
+ },
195
+ },
196
+ };
197
+ }
@@ -0,0 +1,227 @@
1
+ import { countTokens as countTokensGpt5 } from 'gpt-tokenizer/model/gpt-5';
2
+ import { countTokens as countTokensGpt5Pro } from 'gpt-tokenizer/model/gpt-5-pro';
3
+ import { countTokens as countTokensAnthropicRaw } from '@anthropic-ai/tokenizer';
4
+ import { stringifyTokenizerInput } from './tokenStringifier.js';
5
+ export const DEFAULT_MODEL = 'gpt-5.2-pro';
6
+ export const PRO_MODELS = new Set(['gpt-5.1-pro', 'gpt-5-pro', 'gpt-5.2-pro', 'gpt-5.3-pro', 'claude-4.5-sonnet', 'claude-4.6-sonnet', 'claude-4.1-opus', 'claude-4.6-opus']);
7
+ const countTokensAnthropic = (input) => countTokensAnthropicRaw(stringifyTokenizerInput(input));
8
+ export const MODEL_CONFIGS = {
9
+ 'gpt-5.1-pro': {
10
+ model: 'gpt-5.1-pro',
11
+ apiModel: 'gpt-5.2-pro',
12
+ provider: 'openai',
13
+ tokenizer: countTokensGpt5Pro,
14
+ inputLimit: 196000,
15
+ pricing: {
16
+ inputPerToken: 21 / 1_000_000,
17
+ outputPerToken: 168 / 1_000_000,
18
+ },
19
+ reasoning: null,
20
+ },
21
+ 'gpt-5-pro': {
22
+ model: 'gpt-5-pro',
23
+ provider: 'openai',
24
+ tokenizer: countTokensGpt5Pro,
25
+ inputLimit: 196000,
26
+ pricing: {
27
+ inputPerToken: 15 / 1_000_000,
28
+ outputPerToken: 120 / 1_000_000,
29
+ },
30
+ reasoning: null,
31
+ },
32
+ 'gpt-5.1': {
33
+ model: 'gpt-5.1',
34
+ provider: 'openai',
35
+ tokenizer: countTokensGpt5,
36
+ inputLimit: 196000,
37
+ pricing: {
38
+ inputPerToken: 1.25 / 1_000_000,
39
+ outputPerToken: 10 / 1_000_000,
40
+ },
41
+ reasoning: { effort: 'high' },
42
+ },
43
+ 'gpt-5.1-codex': {
44
+ model: 'gpt-5.1-codex',
45
+ provider: 'openai',
46
+ tokenizer: countTokensGpt5,
47
+ inputLimit: 196000,
48
+ pricing: {
49
+ inputPerToken: 1.25 / 1_000_000,
50
+ outputPerToken: 10 / 1_000_000,
51
+ },
52
+ reasoning: { effort: 'high' },
53
+ },
54
+ 'gpt-5.2': {
55
+ model: 'gpt-5.2',
56
+ provider: 'openai',
57
+ tokenizer: countTokensGpt5,
58
+ inputLimit: 196000,
59
+ pricing: {
60
+ inputPerToken: 1.75 / 1_000_000,
61
+ outputPerToken: 14 / 1_000_000,
62
+ },
63
+ reasoning: { effort: 'xhigh' },
64
+ },
65
+ 'gpt-5.2-instant': {
66
+ model: 'gpt-5.2-instant',
67
+ apiModel: 'gpt-5.2-chat-latest',
68
+ provider: 'openai',
69
+ tokenizer: countTokensGpt5,
70
+ inputLimit: 196000,
71
+ pricing: {
72
+ inputPerToken: 1.75 / 1_000_000,
73
+ outputPerToken: 14 / 1_000_000,
74
+ },
75
+ reasoning: null,
76
+ },
77
+ 'gpt-5.2-pro': {
78
+ model: 'gpt-5.2-pro',
79
+ provider: 'openai',
80
+ tokenizer: countTokensGpt5Pro,
81
+ inputLimit: 196000,
82
+ pricing: {
83
+ inputPerToken: 21 / 1_000_000,
84
+ outputPerToken: 168 / 1_000_000,
85
+ },
86
+ reasoning: { effort: 'xhigh' },
87
+ },
88
+ 'gpt-5.3': {
89
+ model: 'gpt-5.3',
90
+ provider: 'openai',
91
+ tokenizer: countTokensGpt5,
92
+ inputLimit: 196000,
93
+ pricing: {
94
+ inputPerToken: 2 / 1_000_000,
95
+ outputPerToken: 16 / 1_000_000,
96
+ },
97
+ reasoning: { effort: 'xhigh' },
98
+ },
99
+ 'gpt-5.3-pro': {
100
+ model: 'gpt-5.3-pro',
101
+ provider: 'openai',
102
+ tokenizer: countTokensGpt5Pro,
103
+ inputLimit: 196000,
104
+ pricing: {
105
+ inputPerToken: 25 / 1_000_000,
106
+ outputPerToken: 200 / 1_000_000,
107
+ },
108
+ reasoning: { effort: 'xhigh' },
109
+ },
110
+ 'gemini-3-pro': {
111
+ model: 'gemini-3-pro',
112
+ provider: 'google',
113
+ tokenizer: countTokensGpt5Pro,
114
+ inputLimit: 200000,
115
+ pricing: {
116
+ inputPerToken: 2 / 1_000_000,
117
+ outputPerToken: 12 / 1_000_000,
118
+ },
119
+ reasoning: null,
120
+ supportsBackground: false,
121
+ supportsSearch: true,
122
+ },
123
+ 'gemini-3.5-pro': {
124
+ model: 'gemini-3.5-pro',
125
+ provider: 'google',
126
+ tokenizer: countTokensGpt5Pro,
127
+ inputLimit: 200000,
128
+ pricing: {
129
+ inputPerToken: 3 / 1_000_000,
130
+ outputPerToken: 18 / 1_000_000,
131
+ },
132
+ reasoning: null,
133
+ supportsBackground: false,
134
+ supportsSearch: true,
135
+ },
136
+ 'claude-4.5-sonnet': {
137
+ model: 'claude-4.5-sonnet',
138
+ apiModel: 'claude-sonnet-4-5',
139
+ provider: 'anthropic',
140
+ tokenizer: countTokensAnthropic,
141
+ inputLimit: 200000,
142
+ pricing: {
143
+ inputPerToken: 3 / 1_000_000,
144
+ outputPerToken: 15 / 1_000_000,
145
+ },
146
+ reasoning: null,
147
+ supportsBackground: false,
148
+ supportsSearch: false,
149
+ },
150
+ 'claude-4.1-opus': {
151
+ model: 'claude-4.1-opus',
152
+ apiModel: 'claude-opus-4-1',
153
+ provider: 'anthropic',
154
+ tokenizer: countTokensAnthropic,
155
+ inputLimit: 200000,
156
+ pricing: {
157
+ inputPerToken: 15 / 1_000_000,
158
+ outputPerToken: 75 / 1_000_000,
159
+ },
160
+ reasoning: { effort: 'high' },
161
+ supportsBackground: false,
162
+ supportsSearch: false,
163
+ },
164
+ 'claude-4.6-sonnet': {
165
+ model: 'claude-4.6-sonnet',
166
+ apiModel: 'claude-sonnet-4-6',
167
+ provider: 'anthropic',
168
+ tokenizer: countTokensAnthropic,
169
+ inputLimit: 200000,
170
+ pricing: {
171
+ inputPerToken: 3 / 1_000_000,
172
+ outputPerToken: 15 / 1_000_000,
173
+ },
174
+ reasoning: null,
175
+ supportsBackground: false,
176
+ supportsSearch: false,
177
+ },
178
+ 'claude-4.6-opus': {
179
+ model: 'claude-4.6-opus',
180
+ apiModel: 'claude-opus-4-6',
181
+ provider: 'anthropic',
182
+ tokenizer: countTokensAnthropic,
183
+ inputLimit: 200000,
184
+ pricing: {
185
+ inputPerToken: 15 / 1_000_000,
186
+ outputPerToken: 75 / 1_000_000,
187
+ },
188
+ reasoning: { effort: 'high' },
189
+ supportsBackground: false,
190
+ supportsSearch: false,
191
+ },
192
+ 'grok-4.1': {
193
+ model: 'grok-4.1',
194
+ apiModel: 'grok-4-1-fast-reasoning',
195
+ provider: 'xai',
196
+ tokenizer: countTokensGpt5Pro,
197
+ inputLimit: 2_000_000,
198
+ pricing: {
199
+ inputPerToken: 0.2 / 1_000_000,
200
+ outputPerToken: 0.5 / 1_000_000,
201
+ },
202
+ reasoning: null,
203
+ supportsBackground: false,
204
+ supportsSearch: true,
205
+ searchToolType: 'web_search',
206
+ },
207
+ 'grok-4.2': {
208
+ model: 'grok-4.2',
209
+ apiModel: 'grok-4-2',
210
+ provider: 'xai',
211
+ tokenizer: countTokensGpt5Pro,
212
+ inputLimit: 2_000_000,
213
+ pricing: {
214
+ inputPerToken: 0.3 / 1_000_000,
215
+ outputPerToken: 1.0 / 1_000_000,
216
+ },
217
+ reasoning: null,
218
+ supportsBackground: false,
219
+ supportsSearch: true,
220
+ searchToolType: 'web_search',
221
+ },
222
+ };
223
+ export const DEFAULT_SYSTEM_PROMPT = [
224
+ 'You are Oracle, a focused one-shot problem solver.',
225
+ 'Emphasize direct answers and cite any files referenced.',
226
+ ].join(' ');
227
+ export const TOKENIZER_OPTIONS = { allowedSpecial: 'all' };