@lowire/loop 0.0.12 → 0.0.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/lib/loop.d.ts CHANGED
@@ -55,8 +55,8 @@ export declare class Loop {
55
55
  private _provider;
56
56
  private _loopOptions;
57
57
  private _cacheOutput;
58
- constructor(loopName: 'openai' | 'github' | 'anthropic' | 'google', options: LoopOptions);
59
- run(task: string, runOptions?: Omit<LoopOptions, 'model'> & {
58
+ constructor(options: LoopOptions);
59
+ run(task: string, runOptions?: Omit<LoopOptions, 'model' | 'api' | 'apiKey'> & {
60
60
  model?: string;
61
61
  }): Promise<{
62
62
  result?: types.ToolResult;
package/lib/loop.js CHANGED
@@ -23,8 +23,8 @@ class Loop {
23
23
  _provider;
24
24
  _loopOptions;
25
25
  _cacheOutput = {};
26
- constructor(loopName, options) {
27
- this._provider = (0, registry_1.getProvider)(loopName);
26
+ constructor(options) {
27
+ this._provider = (0, registry_1.getProvider)(options.api);
28
28
  this._loopOptions = options;
29
29
  }
30
30
  async run(task, runOptions = {}) {
@@ -27,7 +27,7 @@ class Anthropic {
27
27
  system: systemPrompt(conversation.systemPrompt),
28
28
  messages: conversation.messages.map(toAnthropicMessageParts).flat(),
29
29
  tools: conversation.tools.map(toAnthropicTool),
30
- thinking: options.reasoning ? {
30
+ thinking: options.reasoning !== 'none' ? {
31
31
  type: 'enabled',
32
32
  budget_tokens: options.maxTokens ? Math.round(maxTokens / 10) : 1024,
33
33
  } : undefined,
@@ -44,12 +44,12 @@ exports.Anthropic = Anthropic;
44
44
  async function create(createParams, options) {
45
45
  const headers = {
46
46
  'Content-Type': 'application/json',
47
- 'x-api-key': process.env.ANTHROPIC_API_KEY,
47
+ 'x-api-key': options.apiKey,
48
48
  'anthropic-version': '2023-06-01',
49
49
  };
50
50
  const debugBody = { ...createParams, tools: `${createParams.tools?.length ?? 0} tools` };
51
51
  options.debug?.('lowire:anthropic')('Request:', JSON.stringify(debugBody, null, 2));
52
- const response = await fetch(`https://api.anthropic.com/v1/messages`, {
52
+ const response = await fetch(options.apiEndpoint ?? `https://api.anthropic.com/v1/messages`, {
53
53
  method: 'POST',
54
54
  headers,
55
55
  body: JSON.stringify(createParams)
@@ -44,16 +44,13 @@ class Google {
44
44
  }
45
45
  exports.Google = Google;
46
46
  async function create(model, createParams, options) {
47
- const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
48
- if (!apiKey)
49
- throw new Error('GEMINI_API_KEY or GOOGLE_API_KEY environment variable is required');
50
47
  const debugBody = { ...createParams, tools: `${createParams.tools?.length ?? 0} tools` };
51
48
  options.debug?.('lowire:google')('Request:', JSON.stringify(debugBody, null, 2));
52
- const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`, {
49
+ const response = await fetch(options.apiEndpoint ?? `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`, {
53
50
  method: 'POST',
54
51
  headers: {
55
52
  'Content-Type': 'application/json',
56
- 'x-goog-api-key': apiKey,
53
+ 'x-goog-api-key': options.apiKey,
57
54
  },
58
55
  body: JSON.stringify(createParams)
59
56
  });
@@ -16,171 +16,14 @@
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.OpenAI = void 0;
19
+ const openaiCompletions_1 = require("./openaiCompletions");
20
+ const openaiResponses_1 = require("./openaiResponses");
19
21
  class OpenAI {
20
22
  name = 'openai';
21
23
  async complete(conversation, options) {
22
- const inputItems = conversation.messages.map(toResponseInputItems).flat();
23
- const tools = conversation.tools.map(toOpenAIFunctionTool);
24
- const response = await create({
25
- model: options.model,
26
- temperature: options.temperature,
27
- input: inputItems,
28
- instructions: systemPrompt(conversation.systemPrompt),
29
- tools: tools.length > 0 ? tools : undefined,
30
- tool_choice: conversation.tools.length > 0 ? 'auto' : undefined,
31
- parallel_tool_calls: false,
32
- }, options);
33
- // Parse response output items
34
- const result = { role: 'assistant', content: [] };
35
- for (const item of response.output) {
36
- if (item.type === 'message' && item.role === 'assistant') {
37
- result.openaiId = item.id;
38
- result.openaiStatus = item.status;
39
- for (const contentPart of item.content) {
40
- if (contentPart.type === 'output_text') {
41
- result.content.push({
42
- type: 'text',
43
- text: contentPart.text,
44
- });
45
- }
46
- }
47
- }
48
- else if (item.type === 'function_call') {
49
- // Add tool call
50
- result.content.push(toToolCall(item));
51
- }
52
- }
53
- const usage = {
54
- input: response.usage?.input_tokens ?? 0,
55
- output: response.usage?.output_tokens ?? 0,
56
- };
57
- return { result, usage };
24
+ if (options.apiVersion === 'v1/chat/completions')
25
+ return (0, openaiCompletions_1.complete)(conversation, options);
26
+ return (0, openaiResponses_1.complete)(conversation, options);
58
27
  }
59
28
  }
60
29
  exports.OpenAI = OpenAI;
61
- async function create(createParams, options) {
62
- const apiKey = process.env.OPENAI_API_KEY;
63
- const headers = {
64
- 'Content-Type': 'application/json',
65
- 'Authorization': `Bearer ${apiKey}`,
66
- 'Copilot-Vision-Request': 'true',
67
- };
68
- const debugBody = { ...createParams, tools: `${createParams.tools?.length ?? 0} tools` };
69
- options.debug?.('lowire:openai-responses')('Request:', JSON.stringify(debugBody, null, 2));
70
- const response = await fetch(`https://api.openai.com/v1/responses`, {
71
- method: 'POST',
72
- headers,
73
- body: JSON.stringify(createParams)
74
- });
75
- if (!response.ok) {
76
- options.debug?.('lowire:openai-responses')('Response:', response.status);
77
- throw new Error(`API error: ${response.status} ${response.statusText} ${await response.text()}`);
78
- }
79
- const responseBody = await response.json();
80
- options.debug?.('lowire:openai-responses')('Response:', JSON.stringify(responseBody, null, 2));
81
- return responseBody;
82
- }
83
- function toResultContentPart(part) {
84
- if (part.type === 'text') {
85
- return {
86
- type: 'input_text',
87
- text: part.text,
88
- };
89
- }
90
- if (part.type === 'image') {
91
- return {
92
- type: 'input_image',
93
- image_url: `data:${part.mimeType};base64,${part.data}`,
94
- detail: 'auto',
95
- };
96
- }
97
- throw new Error(`Cannot convert content part of type ${part.type} to response content part`);
98
- }
99
- function toResponseInputItems(message) {
100
- if (message.role === 'user') {
101
- return [{
102
- type: 'message',
103
- role: 'user',
104
- content: message.content
105
- }];
106
- }
107
- if (message.role === 'assistant') {
108
- const textParts = message.content.filter(part => part.type === 'text');
109
- const toolCallParts = message.content.filter(part => part.type === 'tool_call');
110
- const items = [];
111
- // Add assistant message with text content
112
- if (textParts.length > 0) {
113
- const outputMessage = {
114
- id: message.openaiId,
115
- status: message.openaiStatus,
116
- type: 'message',
117
- role: 'assistant',
118
- content: textParts.map(part => ({
119
- type: 'output_text',
120
- text: part.text,
121
- annotations: [],
122
- logprobs: []
123
- }))
124
- };
125
- items.push(outputMessage);
126
- }
127
- if (message.toolError) {
128
- items.push({
129
- type: 'message',
130
- role: 'user',
131
- content: message.toolError
132
- });
133
- }
134
- items.push(...toolCallParts.map(toFunctionToolCall).flat());
135
- return items;
136
- }
137
- throw new Error(`Unsupported message role: ${message.role}`);
138
- }
139
- function toOpenAIFunctionTool(tool) {
140
- return {
141
- type: 'function',
142
- name: tool.name,
143
- description: tool.description ?? null,
144
- parameters: tool.inputSchema,
145
- strict: null,
146
- };
147
- }
148
- function toFunctionToolCall(toolCall) {
149
- const result = [{
150
- type: 'function_call',
151
- call_id: toolCall.id,
152
- name: toolCall.name,
153
- arguments: JSON.stringify(toolCall.arguments),
154
- id: toolCall.openaiId,
155
- status: toolCall.openaiStatus,
156
- }];
157
- if (toolCall.result) {
158
- result.push({
159
- type: 'function_call_output',
160
- call_id: toolCall.id,
161
- output: toolCall.result.content.map(toResultContentPart),
162
- });
163
- }
164
- return result;
165
- }
166
- function toToolCall(functionCall) {
167
- return {
168
- type: 'tool_call',
169
- name: functionCall.name,
170
- arguments: JSON.parse(functionCall.arguments),
171
- id: functionCall.call_id,
172
- openaiId: functionCall.id,
173
- openaiStatus: functionCall.status,
174
- };
175
- }
176
- const systemPrompt = (prompt) => `
177
- ### System instructions
178
-
179
- ${prompt}
180
-
181
- ### Tool calling instructions
182
- - Make sure every message contains a tool call.
183
- - When you use a tool, you may provide a brief thought or explanation in the content field
184
- immediately before the tool_call. Do not split this into separate messages.
185
- - Every reply must include a tool call.
186
- `;
@@ -13,4 +13,8 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- export {};
16
+ import type * as types from '../types';
17
+ export declare function complete(conversation: types.Conversation, options: types.CompletionOptions): Promise<{
18
+ result: types.AssistantMessage;
19
+ usage: types.Usage;
20
+ }>;
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) Microsoft Corporation.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.complete = complete;
19
+ async function complete(conversation, options) {
20
+ // Convert generic messages to OpenAI format
21
+ const systemMessage = {
22
+ role: 'system',
23
+ content: systemPrompt(conversation.systemPrompt)
24
+ };
25
+ const openaiMessages = [systemMessage, ...conversation.messages.map(toCompletionsMessages).flat()];
26
+ const openaiTools = conversation.tools.map(t => toCompletionsTool(t));
27
+ const response = await create({
28
+ model: options.model,
29
+ max_tokens: options.maxTokens,
30
+ temperature: options.temperature,
31
+ messages: openaiMessages,
32
+ tools: openaiTools,
33
+ tool_choice: conversation.tools.length > 0 ? 'auto' : undefined,
34
+ reasoning_effort: toCompletionsReasoning(options.reasoning),
35
+ parallel_tool_calls: false,
36
+ }, options);
37
+ if (!response || !response.choices.length)
38
+ throw new Error('Failed to get response from OpenAI completions');
39
+ const result = { role: 'assistant', content: [] };
40
+ for (const choice of response.choices) {
41
+ const message = choice.message;
42
+ if (message.content)
43
+ result.content.push({ type: 'text', text: message.content });
44
+ for (const entry of message.tool_calls || []) {
45
+ if (entry.type !== 'function')
46
+ continue;
47
+ result.content.push(toToolCall(entry));
48
+ }
49
+ }
50
+ const usage = {
51
+ input: response.usage?.prompt_tokens ?? 0,
52
+ output: response.usage?.completion_tokens ?? 0,
53
+ };
54
+ return { result, usage };
55
+ }
56
+ async function create(createParams, options) {
57
+ const headers = {
58
+ 'Content-Type': 'application/json',
59
+ 'Authorization': `Bearer ${options.apiKey}`,
60
+ };
61
+ const debugBody = { ...createParams, tools: `${createParams.tools?.length ?? 0} tools` };
62
+ options.debug?.('lowire:openai')('Request:', JSON.stringify(debugBody, null, 2));
63
+ const response = await fetch(options.apiEndpoint ?? `https://api.openai.com/v1/chat/completions`, {
64
+ method: 'POST',
65
+ headers,
66
+ body: JSON.stringify(createParams),
67
+ });
68
+ if (!response.ok) {
69
+ options.debug?.('lowire:openai')('Response:', response.status);
70
+ throw new Error(`API error: ${response.status} ${response.statusText} ${await response.text()}`);
71
+ }
72
+ const responseBody = await response.json();
73
+ options.debug?.('lowire:openai')('Response:', JSON.stringify(responseBody, null, 2));
74
+ return responseBody;
75
+ }
76
+ function toCopilotResultContentPart(part) {
77
+ if (part.type === 'text') {
78
+ return {
79
+ type: 'text',
80
+ text: part.text,
81
+ };
82
+ }
83
+ if (part.type === 'image') {
84
+ return {
85
+ type: 'image_url',
86
+ image_url: {
87
+ url: `data:${part.mimeType};base64,${part.data}`,
88
+ },
89
+ };
90
+ }
91
+ throw new Error(`Cannot convert content part of type ${part.type} to text content part`);
92
+ }
93
+ function toCompletionsMessages(message) {
94
+ if (message.role === 'user') {
95
+ return [{
96
+ role: 'user',
97
+ content: message.content
98
+ }];
99
+ }
100
+ if (message.role === 'assistant') {
101
+ const assistantMessage = {
102
+ role: 'assistant'
103
+ };
104
+ const textParts = message.content.filter(part => part.type === 'text');
105
+ const toolCallParts = message.content.filter(part => part.type === 'tool_call');
106
+ if (textParts.length === 1)
107
+ assistantMessage.content = textParts[0].text;
108
+ else
109
+ assistantMessage.content = textParts;
110
+ const toolCalls = [];
111
+ const toolResultMessages = [];
112
+ for (const toolCall of toolCallParts) {
113
+ toolCalls.push({
114
+ id: toolCall.id,
115
+ type: 'function',
116
+ function: {
117
+ name: toolCall.name,
118
+ arguments: JSON.stringify(toolCall.arguments)
119
+ }
120
+ });
121
+ if (toolCall.result) {
122
+ toolResultMessages.push({
123
+ role: 'tool',
124
+ tool_call_id: toolCall.id,
125
+ content: toolCall.result.content.map(toCopilotResultContentPart),
126
+ });
127
+ }
128
+ }
129
+ if (toolCalls.length > 0)
130
+ assistantMessage.tool_calls = toolCalls;
131
+ if (message.toolError) {
132
+ toolResultMessages.push({
133
+ role: 'user',
134
+ content: [{
135
+ type: 'text',
136
+ text: message.toolError,
137
+ }]
138
+ });
139
+ }
140
+ return [assistantMessage, ...toolResultMessages];
141
+ }
142
+ throw new Error(`Unsupported message role: ${message.role}`);
143
+ }
144
+ function toCompletionsTool(tool) {
145
+ return {
146
+ type: 'function',
147
+ function: {
148
+ name: tool.name,
149
+ description: tool.description,
150
+ parameters: tool.inputSchema,
151
+ },
152
+ };
153
+ }
154
+ function toToolCall(entry) {
155
+ return {
156
+ type: 'tool_call',
157
+ name: entry.type === 'function' ? entry.function.name : entry.custom.name,
158
+ arguments: JSON.parse(entry.type === 'function' ? entry.function.arguments : entry.custom.input),
159
+ id: entry.id,
160
+ };
161
+ }
162
+ function toCompletionsReasoning(reasoning) {
163
+ switch (reasoning) {
164
+ case 'none':
165
+ return 'none';
166
+ case 'medium':
167
+ return 'medium';
168
+ case 'high':
169
+ return 'high';
170
+ }
171
+ }
172
+ const systemPrompt = (prompt) => `
173
+ ### System instructions
174
+
175
+ ${prompt}
176
+
177
+ ### Tool calling instructions
178
+ - Make sure every message contains a tool call.
179
+ - When you use a tool, you may provide a brief thought or explanation in the content field
180
+ immediately before the tool_call. Do not split this into separate messages.
181
+ - Every reply must include a tool call.
182
+ `;
@@ -14,20 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import type * as types from '../types';
17
- export declare class Github implements types.Provider {
18
- readonly name: string;
19
- private _apiKey;
20
- private _bearer;
21
- complete(conversation: types.Conversation, options: types.CompletionOptions): Promise<{
22
- result: types.AssistantMessage;
23
- usage: types.Usage;
24
- }>;
25
- }
26
- export declare const kEditorHeaders: {
27
- 'Editor-Version': string;
28
- 'Editor-Plugin-Version': string;
29
- 'User-Agent': string;
30
- Accept: string;
31
- 'Content-Type': string;
32
- 'Copilot-Vision-Request': string;
33
- };
17
+ export declare function complete(conversation: types.Conversation, options: types.CompletionOptions): Promise<{
18
+ result: types.AssistantMessage;
19
+ usage: types.Usage;
20
+ }>;
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) Microsoft Corporation.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.complete = complete;
19
+ async function complete(conversation, options) {
20
+ const inputItems = conversation.messages.map(toResponseInputItems).flat();
21
+ const tools = conversation.tools.map(toOpenAIFunctionTool);
22
+ const response = await create({
23
+ model: options.model,
24
+ temperature: options.temperature,
25
+ input: inputItems,
26
+ instructions: systemPrompt(conversation.systemPrompt),
27
+ tools: tools.length > 0 ? tools : undefined,
28
+ tool_choice: conversation.tools.length > 0 ? 'auto' : undefined,
29
+ parallel_tool_calls: false,
30
+ reasoning: toOpenAIReasoning(options.reasoning),
31
+ }, options);
32
+ // Parse response output items
33
+ const result = { role: 'assistant', content: [] };
34
+ for (const item of response.output) {
35
+ if (item.type === 'message' && item.role === 'assistant') {
36
+ result.openaiId = item.id;
37
+ result.openaiStatus = item.status;
38
+ for (const contentPart of item.content) {
39
+ if (contentPart.type === 'output_text') {
40
+ result.content.push({
41
+ type: 'text',
42
+ text: contentPart.text,
43
+ });
44
+ }
45
+ }
46
+ }
47
+ else if (item.type === 'function_call') {
48
+ // Add tool call
49
+ result.content.push(toToolCall(item));
50
+ }
51
+ }
52
+ const usage = {
53
+ input: response.usage?.input_tokens ?? 0,
54
+ output: response.usage?.output_tokens ?? 0,
55
+ };
56
+ return { result, usage };
57
+ }
58
+ async function create(createParams, options) {
59
+ const headers = {
60
+ 'Content-Type': 'application/json',
61
+ 'Authorization': `Bearer ${options.apiKey}`,
62
+ };
63
+ const debugBody = { ...createParams, tools: `${createParams.tools?.length ?? 0} tools` };
64
+ options.debug?.('lowire:openai-responses')('Request:', JSON.stringify(debugBody, null, 2));
65
+ const response = await fetch(options.apiEndpoint ?? `https://api.openai.com/v1/responses`, {
66
+ method: 'POST',
67
+ headers,
68
+ body: JSON.stringify(createParams)
69
+ });
70
+ if (!response.ok) {
71
+ options.debug?.('lowire:openai-responses')('Response:', response.status);
72
+ throw new Error(`API error: ${response.status} ${response.statusText} ${await response.text()}`);
73
+ }
74
+ const responseBody = await response.json();
75
+ options.debug?.('lowire:openai-responses')('Response:', JSON.stringify(responseBody, null, 2));
76
+ return responseBody;
77
+ }
78
+ function toResultContentPart(part) {
79
+ if (part.type === 'text') {
80
+ return {
81
+ type: 'input_text',
82
+ text: part.text,
83
+ };
84
+ }
85
+ if (part.type === 'image') {
86
+ return {
87
+ type: 'input_image',
88
+ image_url: `data:${part.mimeType};base64,${part.data}`,
89
+ detail: 'auto',
90
+ };
91
+ }
92
+ throw new Error(`Cannot convert content part of type ${part.type} to response content part`);
93
+ }
94
+ function toResponseInputItems(message) {
95
+ if (message.role === 'user') {
96
+ return [{
97
+ type: 'message',
98
+ role: 'user',
99
+ content: message.content
100
+ }];
101
+ }
102
+ if (message.role === 'assistant') {
103
+ const textParts = message.content.filter(part => part.type === 'text');
104
+ const toolCallParts = message.content.filter(part => part.type === 'tool_call');
105
+ const items = [];
106
+ // Add assistant message with text content
107
+ if (textParts.length > 0) {
108
+ const outputMessage = {
109
+ id: message.openaiId,
110
+ status: message.openaiStatus,
111
+ type: 'message',
112
+ role: 'assistant',
113
+ content: textParts.map(part => ({
114
+ type: 'output_text',
115
+ text: part.text,
116
+ annotations: [],
117
+ logprobs: []
118
+ }))
119
+ };
120
+ items.push(outputMessage);
121
+ }
122
+ if (message.toolError) {
123
+ items.push({
124
+ type: 'message',
125
+ role: 'user',
126
+ content: message.toolError
127
+ });
128
+ }
129
+ items.push(...toolCallParts.map(toFunctionToolCall).flat());
130
+ return items;
131
+ }
132
+ throw new Error(`Unsupported message role: ${message.role}`);
133
+ }
134
+ function toOpenAIFunctionTool(tool) {
135
+ return {
136
+ type: 'function',
137
+ name: tool.name,
138
+ description: tool.description ?? null,
139
+ parameters: tool.inputSchema,
140
+ strict: null,
141
+ };
142
+ }
143
+ function toFunctionToolCall(toolCall) {
144
+ const result = [{
145
+ type: 'function_call',
146
+ call_id: toolCall.id,
147
+ name: toolCall.name,
148
+ arguments: JSON.stringify(toolCall.arguments),
149
+ id: toolCall.openaiId,
150
+ status: toolCall.openaiStatus,
151
+ }];
152
+ if (toolCall.result) {
153
+ result.push({
154
+ type: 'function_call_output',
155
+ call_id: toolCall.id,
156
+ output: toolCall.result.content.map(toResultContentPart),
157
+ });
158
+ }
159
+ return result;
160
+ }
161
+ function toToolCall(functionCall) {
162
+ return {
163
+ type: 'tool_call',
164
+ name: functionCall.name,
165
+ arguments: JSON.parse(functionCall.arguments),
166
+ id: functionCall.call_id,
167
+ openaiId: functionCall.id,
168
+ openaiStatus: functionCall.status,
169
+ };
170
+ }
171
+ function toOpenAIReasoning(reasoning) {
172
+ switch (reasoning) {
173
+ case 'none':
174
+ return { effort: 'none' };
175
+ case 'medium':
176
+ return { effort: 'medium' };
177
+ case 'high':
178
+ return { effort: 'high' };
179
+ }
180
+ }
181
+ const systemPrompt = (prompt) => `
182
+ ### System instructions
183
+
184
+ ${prompt}
185
+
186
+ ### Tool calling instructions
187
+ - Make sure every message contains a tool call.
188
+ - When you use a tool, you may provide a brief thought or explanation in the content field
189
+ immediately before the tool_call. Do not split this into separate messages.
190
+ - Every reply must include a tool call.
191
+ `;
@@ -13,5 +13,5 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import * as types from '../types';
17
- export declare function getProvider(loopName: 'openai' | 'github' | 'anthropic' | 'google'): types.Provider;
16
+ import type * as types from '../types';
17
+ export declare function getProvider(api: 'openai' | 'anthropic' | 'google'): types.Provider;
@@ -16,18 +16,15 @@
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.getProvider = getProvider;
19
- const openai_1 = require("./openai");
20
- const github_1 = require("./github");
21
19
  const anthropic_1 = require("./anthropic");
22
20
  const google_1 = require("./google");
23
- function getProvider(loopName) {
24
- if (loopName === 'openai')
21
+ const openai_1 = require("./openai");
22
+ function getProvider(api) {
23
+ if (api === 'openai')
25
24
  return new openai_1.OpenAI();
26
- if (loopName === 'github')
27
- return new github_1.Github();
28
- if (loopName === 'anthropic')
25
+ if (api === 'anthropic')
29
26
  return new anthropic_1.Anthropic();
30
- if (loopName === 'google')
27
+ if (api === 'google')
31
28
  return new google_1.Google();
32
- throw new Error(`Unknown loop LLM: ${loopName}`);
29
+ throw new Error(`Unknown loop LLM: ${api}`);
33
30
  }
package/lib/types.d.ts CHANGED
@@ -46,7 +46,6 @@ export type TextContentPart = {
46
46
  type: 'text';
47
47
  text: string;
48
48
  googleThoughtSignature?: string;
49
- copilotToolCallId?: string;
50
49
  };
51
50
  export type ThinkingContentPart = {
52
51
  type: 'thinking';
@@ -91,9 +90,13 @@ export type Conversation = {
91
90
  };
92
91
  export type Debug = (category: string) => (...args: any[]) => void;
93
92
  export type CompletionOptions = {
93
+ api: 'openai' | 'anthropic' | 'google';
94
+ apiEndpoint?: string;
95
+ apiKey: string;
96
+ apiVersion?: string;
94
97
  model: string;
95
98
  maxTokens?: number;
96
- reasoning?: boolean;
99
+ reasoning?: 'none' | 'medium' | 'high';
97
100
  temperature?: number;
98
101
  debug?: Debug;
99
102
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowire/loop",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Small agentic loop",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,81 +0,0 @@
1
- "use strict";
2
- /**
3
- * Copyright (c) Microsoft Corporation.
4
- *
5
- * Licensed under the Apache License, Version 2.0 (the "License");
6
- * you may not use this file except in compliance with the License.
7
- * You may obtain a copy of the License at
8
- *
9
- * http://www.apache.org/licenses/LICENSE-2.0
10
- *
11
- * Unless required by applicable law or agreed to in writing, software
12
- * distributed under the License is distributed on an "AS IS" BASIS,
13
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- * See the License for the specific language governing permissions and
15
- * limitations under the License.
16
- */
17
- Object.defineProperty(exports, "__esModule", { value: true });
18
- const github_1 = require("../providers/github");
19
- /* eslint-disable no-console */
20
- // The Client ID for VS Code. This is public knowledge but technically "internal" to VS Code.
21
- // Using this ID allows the script to impersonate VS Code to get the correct scopes.
22
- const CLIENT_ID = 'Iv1.b507a08c87ecfe98';
23
- const SCOPE = 'read:user share:copilot';
24
- async function initiateDeviceFlow() {
25
- const response = await fetch('https://github.com/login/device/code', {
26
- method: 'POST',
27
- headers: github_1.kEditorHeaders,
28
- body: JSON.stringify({
29
- client_id: CLIENT_ID,
30
- scope: SCOPE
31
- })
32
- });
33
- return await response.json();
34
- }
35
- async function pollForToken(deviceCode, interval) {
36
- console.log('Waiting for user authorization...');
37
- return new Promise((resolve, reject) => {
38
- const pollInterval = setInterval(async () => {
39
- try {
40
- const response = await fetch('https://github.com/login/oauth/access_token', {
41
- method: 'POST',
42
- headers: github_1.kEditorHeaders,
43
- body: JSON.stringify({
44
- client_id: CLIENT_ID,
45
- device_code: deviceCode,
46
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
47
- })
48
- });
49
- const data = await response.json();
50
- if ('access_token' in data) {
51
- clearInterval(pollInterval);
52
- resolve(data.access_token);
53
- }
54
- else if (data.error === 'authorization_pending') {
55
- process.stdout.write('.');
56
- }
57
- else if (data.error === 'slow_down') {
58
- console.log('(slow down)');
59
- }
60
- else {
61
- clearInterval(pollInterval);
62
- reject(new Error(data.error_description || data.error));
63
- }
64
- }
65
- catch (error) {
66
- clearInterval(pollInterval);
67
- reject(error);
68
- }
69
- }, (interval + 1) * 1000);
70
- });
71
- }
72
- void (async () => {
73
- const deviceData = await initiateDeviceFlow();
74
- console.log('\n**************************************************');
75
- console.log(`Please go to: ${deviceData.verification_uri}`);
76
- console.log(`And enter code: ${deviceData.user_code}`);
77
- console.log('**************************************************\n');
78
- const oauthToken = await pollForToken(deviceData.device_code, deviceData.interval);
79
- console.log('\n✔ Authentication successful!');
80
- console.log(`COPILOT_API_KEY=${oauthToken}`);
81
- })();
@@ -1,231 +0,0 @@
1
- "use strict";
2
- /**
3
- * Copyright (c) Microsoft Corporation.
4
- *
5
- * Licensed under the Apache License, Version 2.0 (the "License");
6
- * you may not use this file except in compliance with the License.
7
- * You may obtain a copy of the License at
8
- *
9
- * http://www.apache.org/licenses/LICENSE-2.0
10
- *
11
- * Unless required by applicable law or agreed to in writing, software
12
- * distributed under the License is distributed on an "AS IS" BASIS,
13
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- * See the License for the specific language governing permissions and
15
- * limitations under the License.
16
- */
17
- Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.kEditorHeaders = exports.Github = void 0;
19
- class Github {
20
- name = 'github';
21
- _apiKey;
22
- async _bearer() {
23
- if (!this._apiKey)
24
- this._apiKey = await getCopilotToken();
25
- return this._apiKey;
26
- }
27
- async complete(conversation, options) {
28
- // Convert generic messages to OpenAI format
29
- const systemMessage = {
30
- role: 'system',
31
- content: systemPrompt(conversation.systemPrompt)
32
- };
33
- const openaiMessages = [systemMessage, ...conversation.messages.map(toCopilotMessages).flat()];
34
- const openaiTools = conversation.tools.map(t => toCopilotTool(t));
35
- const bearer = await this._bearer();
36
- let response;
37
- // Github provider is unreliable, retry up to 3 times.
38
- for (let i = 0; i < 3; ++i) {
39
- response = await create({
40
- model: options.model,
41
- max_tokens: options.maxTokens,
42
- temperature: options.temperature,
43
- messages: openaiMessages,
44
- tools: openaiTools,
45
- tool_choice: conversation.tools.length > 0 ? 'auto' : undefined,
46
- reasoning_effort: options.reasoning ? 'medium' : undefined,
47
- parallel_tool_calls: false,
48
- }, bearer, options);
49
- if (response.choices.length)
50
- break;
51
- }
52
- if (!response || !response.choices.length)
53
- throw new Error('Failed to get response from GitHub Copilot');
54
- const result = { role: 'assistant', content: [] };
55
- for (const choice of response.choices) {
56
- const message = choice.message;
57
- if (message.content)
58
- result.content.push({ type: 'text', text: message.content });
59
- for (const entry of message.tool_calls || []) {
60
- if (entry.type !== 'function')
61
- continue;
62
- const { toolCall, intent } = toToolCall(entry);
63
- if (intent)
64
- result.content.push({ type: 'text', text: intent, copilotToolCallId: toolCall.id });
65
- result.content.push(toolCall);
66
- }
67
- }
68
- const usage = {
69
- input: response.usage?.prompt_tokens ?? 0,
70
- output: response.usage?.completion_tokens ?? 0,
71
- };
72
- return { result, usage };
73
- }
74
- }
75
- exports.Github = Github;
76
- async function create(createParams, bearer, options) {
77
- const headers = {
78
- 'Authorization': `Bearer ${bearer}`,
79
- ...exports.kEditorHeaders,
80
- };
81
- const debugBody = { ...createParams, tools: `${createParams.tools?.length ?? 0} tools` };
82
- options.debug?.('lowire:github')('Request:', JSON.stringify(debugBody, null, 2));
83
- const response = await fetch(`https://api.githubcopilot.com/chat/completions`, {
84
- method: 'POST',
85
- headers,
86
- body: JSON.stringify(createParams),
87
- });
88
- if (!response.ok) {
89
- options.debug?.('lowire:github')('Response:', response.status);
90
- throw new Error(`API error: ${response.status} ${response.statusText} ${await response.text()}`);
91
- }
92
- const responseBody = await response.json();
93
- options.debug?.('lowire:github')('Response:', JSON.stringify(responseBody, null, 2));
94
- return responseBody;
95
- }
96
- function toCopilotResultContentPart(part) {
97
- if (part.type === 'text') {
98
- return {
99
- type: 'text',
100
- text: part.text,
101
- };
102
- }
103
- if (part.type === 'image') {
104
- return {
105
- type: 'image_url',
106
- image_url: {
107
- url: `data:${part.mimeType};base64,${part.data}`,
108
- },
109
- };
110
- }
111
- throw new Error(`Cannot convert content part of type ${part.type} to text content part`);
112
- }
113
- function toCopilotMessages(message) {
114
- if (message.role === 'user') {
115
- return [{
116
- role: 'user',
117
- content: message.content
118
- }];
119
- }
120
- if (message.role === 'assistant') {
121
- const assistantMessage = {
122
- role: 'assistant'
123
- };
124
- const toolIntents = new Map();
125
- for (const part of message.content) {
126
- if (part.type === 'text' && part.copilotToolCallId)
127
- toolIntents.set(part.copilotToolCallId, part.text);
128
- }
129
- const textParts = message.content.filter(part => part.type === 'text' && !part.copilotToolCallId);
130
- const toolCallParts = message.content.filter(part => part.type === 'tool_call');
131
- if (textParts.length === 1)
132
- assistantMessage.content = textParts[0].text;
133
- else
134
- assistantMessage.content = textParts;
135
- const toolCalls = [];
136
- const toolResultMessages = [];
137
- for (const toolCall of toolCallParts) {
138
- const args = { ...toolCall.arguments };
139
- if (toolIntents.has(toolCall.id))
140
- args['_intent'] = toolIntents.get(toolCall.id);
141
- toolCalls.push({
142
- id: toolCall.id,
143
- type: 'function',
144
- function: {
145
- name: toolCall.name,
146
- arguments: JSON.stringify(args)
147
- }
148
- });
149
- if (toolCall.result) {
150
- toolResultMessages.push({
151
- role: 'tool',
152
- tool_call_id: toolCall.id,
153
- content: toolCall.result.content.map(toCopilotResultContentPart),
154
- });
155
- }
156
- }
157
- if (toolCalls.length > 0)
158
- assistantMessage.tool_calls = toolCalls;
159
- if (message.toolError) {
160
- toolResultMessages.push({
161
- role: 'user',
162
- content: [{
163
- type: 'text',
164
- text: message.toolError,
165
- }]
166
- });
167
- }
168
- return [assistantMessage, ...toolResultMessages];
169
- }
170
- throw new Error(`Unsupported message role: ${message.role}`);
171
- }
172
- // Copilot endpoint does not reply with content+tool_call, it instead
173
- // replies with the content and expects continuation. I.e. instead of navigating
174
- // to a page it will reply with "Navigating to <url>" w/o tool call. Mitigate it
175
- // via injecting a tool call intent and then converting it into the assistant
176
- // message content.
177
- function toCopilotTool(tool) {
178
- const parameters = { ...tool.inputSchema };
179
- parameters.properties = {
180
- _intent: { type: 'string', description: 'Describe the intent of this tool call' },
181
- ...parameters.properties || {},
182
- };
183
- return {
184
- type: 'function',
185
- function: {
186
- name: tool.name,
187
- description: tool.description,
188
- parameters,
189
- },
190
- };
191
- }
192
- function toToolCall(entry) {
193
- const toolCall = {
194
- type: 'tool_call',
195
- name: entry.type === 'function' ? entry.function.name : entry.custom.name,
196
- arguments: JSON.parse(entry.type === 'function' ? entry.function.arguments : entry.custom.input),
197
- id: entry.id,
198
- };
199
- const intent = toolCall.arguments['_intent'];
200
- delete toolCall.arguments['_intent'];
201
- return { toolCall, intent };
202
- }
203
- const systemPrompt = (prompt) => `
204
- ### System instructions
205
-
206
- ${prompt}
207
-
208
- ### Tool calling instructions
209
- - Your reply MUST be a tool call and nothing but the tool call.
210
- - NEVER respond with text content, only tool calls.
211
- - Do NOT describe your plan, do NOT explain what you are doing, do NOT describe what you see, call tools.
212
- - Provide thoughts in the '_intent' property of the tool calls instead.
213
- `;
214
- exports.kEditorHeaders = {
215
- 'Editor-Version': 'vscode/1.96.0',
216
- 'Editor-Plugin-Version': 'copilot-chat/0.24.0',
217
- 'User-Agent': 'GitHubCopilotChat/0.24.0',
218
- 'Accept': 'application/json',
219
- 'Content-Type': 'application/json',
220
- 'Copilot-Vision-Request': 'true',
221
- };
222
- async function getCopilotToken() {
223
- const response = await fetch('https://api.github.com/copilot_internal/v2/token', {
224
- method: 'GET',
225
- headers: { 'Authorization': `token ${process.env.COPILOT_API_KEY}`, ...exports.kEditorHeaders }
226
- });
227
- const data = await response.json();
228
- if (data.token)
229
- return data.token;
230
- throw new Error('Failed to get Copilot token');
231
- }