co-maintainer 0.5.1 → 0.5.2
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/dist/package.json +1 -1
- package/dist/src/ai/anthropic.js +6 -1
- package/dist/src/ai/openai.js +18 -19
- package/dist/src/ai/openrouter.js +13 -19
- package/dist/src/ai/provider.d.ts +9 -0
- package/dist/src/ai/provider.js +29 -1
- package/dist/src/knowledge/synthesis.js +3 -0
- package/package.json +1 -1
package/dist/package.json
CHANGED
package/dist/src/ai/anthropic.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getEnv } from "../util/runtime.js";
|
|
2
|
+
import { outputLimitError } from "./provider.js";
|
|
2
3
|
import { CliError, EXIT_RUNTIME, EXIT_USAGE, networkFailure, } from "../cli/error.js";
|
|
3
4
|
/** Required on every Messages request and the only version the errors below
|
|
4
5
|
* are written against. */
|
|
@@ -197,8 +198,12 @@ export function parseMessagesBody(json, model) {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
const usage = json.usage;
|
|
201
|
+
const text = texts.join("");
|
|
202
|
+
if (json.stop_reason === "max_tokens" && !text.trim() && !toolCalls.length) {
|
|
203
|
+
throw outputLimitError("anthropic", model);
|
|
204
|
+
}
|
|
200
205
|
return {
|
|
201
|
-
text
|
|
206
|
+
text,
|
|
202
207
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
203
208
|
// Tokens read from or written to the prompt cache are input too, and with
|
|
204
209
|
// the guides cached they are most of it.
|
package/dist/src/ai/openai.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getEnv } from "../util/runtime.js";
|
|
2
|
+
import { outputLimitError, withoutRefusedPart } from "./provider.js";
|
|
2
3
|
import { CliError, EXIT_RUNTIME, EXIT_USAGE, networkFailure, } from "../cli/error.js";
|
|
3
4
|
/** `CM_OPENAI_URL` points at a local fake in tests. Unset means the real
|
|
4
5
|
* endpoint, so production behavior is unchanged. */
|
|
@@ -145,8 +146,12 @@ function parseResponsesBody(json, model) {
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
const usage = json.usage;
|
|
149
|
+
const text = texts.join("");
|
|
150
|
+
if (json.status === "incomplete" && !text.trim() && !toolCalls.length) {
|
|
151
|
+
throw outputLimitError("openai", model);
|
|
152
|
+
}
|
|
148
153
|
return {
|
|
149
|
-
text
|
|
154
|
+
text,
|
|
150
155
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
151
156
|
tokensIn: Number(usage?.input_tokens ?? 0),
|
|
152
157
|
tokensOut: Number(usage?.output_tokens ?? 0),
|
|
@@ -184,25 +189,19 @@ export class OpenAiProvider {
|
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
191
|
async complete(request) {
|
|
187
|
-
let
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const detail = await response.text();
|
|
193
|
-
if (/text\.format|json_schema|structured/i.test(detail)) {
|
|
194
|
-
const { responseFormat: _dropped, ...withoutSchema } = request;
|
|
195
|
-
response = await this.post(responsesBody(this.model, withoutSchema));
|
|
196
|
-
if (response.ok) {
|
|
197
|
-
return parseResponsesBody((await response.json()), this.model);
|
|
198
|
-
}
|
|
199
|
-
throw openAiError(this.model, response.status, await response.text());
|
|
192
|
+
let current = request;
|
|
193
|
+
for (;;) {
|
|
194
|
+
const response = await this.post(responsesBody(this.model, current));
|
|
195
|
+
if (response.ok) {
|
|
196
|
+
return parseResponsesBody((await response.json()), this.model);
|
|
200
197
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
const detail = await response.text();
|
|
199
|
+
const lighter = response.status === 400
|
|
200
|
+
? withoutRefusedPart(current, detail, /text\.format|json_schema|structured/i)
|
|
201
|
+
: undefined;
|
|
202
|
+
if (!lighter)
|
|
203
|
+
throw openAiError(this.model, response.status, detail);
|
|
204
|
+
current = lighter;
|
|
205
205
|
}
|
|
206
|
-
return parseResponsesBody((await response.json()), this.model);
|
|
207
206
|
}
|
|
208
207
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chatBody, parseChatResponse } from "./provider.js";
|
|
1
|
+
import { chatBody, parseChatResponse, withoutRefusedPart } from "./provider.js";
|
|
2
2
|
import { getEnv } from "../util/runtime.js";
|
|
3
3
|
import { CliError, EXIT_RUNTIME, EXIT_USAGE, networkFailure, } from "../cli/error.js";
|
|
4
4
|
/** `CM_OPENROUTER_URL` points the provider at a local fake in tests. It is
|
|
@@ -75,25 +75,19 @@ export class OpenRouterProvider {
|
|
|
75
75
|
return openRouterError(this.model, status, body, this.host);
|
|
76
76
|
}
|
|
77
77
|
async complete(request) {
|
|
78
|
-
let
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const detail = await response.text();
|
|
84
|
-
if (/response_format|json_schema|structured/i.test(detail)) {
|
|
85
|
-
const { responseFormat: _dropped, ...withoutSchema } = request;
|
|
86
|
-
response = await this.post(chatBody(this.model, withoutSchema));
|
|
87
|
-
if (response.ok) {
|
|
88
|
-
return parseChatResponse((await response.json()), this.host.provider, this.model);
|
|
89
|
-
}
|
|
90
|
-
throw this.fail(response.status, await response.text());
|
|
78
|
+
let current = request;
|
|
79
|
+
for (;;) {
|
|
80
|
+
const response = await this.post(chatBody(this.model, current));
|
|
81
|
+
if (response.ok) {
|
|
82
|
+
return parseChatResponse((await response.json()), this.host.provider, this.model);
|
|
91
83
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
const detail = await response.text();
|
|
85
|
+
const lighter = response.status === 400
|
|
86
|
+
? withoutRefusedPart(current, detail, /response_format|json_schema|structured/i)
|
|
87
|
+
: undefined;
|
|
88
|
+
if (!lighter)
|
|
89
|
+
throw this.fail(response.status, detail);
|
|
90
|
+
current = lighter;
|
|
96
91
|
}
|
|
97
|
-
return parseChatResponse((await response.json()), this.host.provider, this.model);
|
|
98
92
|
}
|
|
99
93
|
}
|
|
@@ -7,5 +7,14 @@ export { providerKeyEnv } from "../config.ts";
|
|
|
7
7
|
/** Hetzner was dropped in 0.5.1. Naming it beats a generic "must be one of". */
|
|
8
8
|
export declare function rejectRetiredProvider(value: string | undefined): void;
|
|
9
9
|
export declare function createAiProvider(options: Options, model: string): AiProvider | undefined;
|
|
10
|
+
/** A 400 that names one optional part of the request earns one retry without
|
|
11
|
+
* it: the JSON schema, which the prompt asks for anyway, or "thinking off",
|
|
12
|
+
* which a model that always thinks refuses. Undefined when nothing is left to
|
|
13
|
+
* drop, so the caller reports the error. */
|
|
14
|
+
export declare function withoutRefusedPart(request: AiRequest, detail: string, schemaPattern: RegExp): AiRequest | undefined;
|
|
15
|
+
/** The model stopped at its output limit before writing a word. Thinking counts
|
|
16
|
+
* toward that limit, so a thinking model can spend all of it there. Without
|
|
17
|
+
* this the empty answer read as a normal one and failed later as "not JSON". */
|
|
18
|
+
export declare function outputLimitError(provider: string, model: string): Error;
|
|
10
19
|
export declare function parseChatResponse(json: Json, provider: AiResponse["provider"], model: string): AiResponse;
|
|
11
20
|
export declare function chatBody(model: string, request: AiRequest): Json;
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -38,6 +38,28 @@ export function createAiProvider(options, model) {
|
|
|
38
38
|
}
|
|
39
39
|
die(`${options.ai} support is not finished yet. Use openrouter or none.`);
|
|
40
40
|
}
|
|
41
|
+
/** A 400 that names one optional part of the request earns one retry without
|
|
42
|
+
* it: the JSON schema, which the prompt asks for anyway, or "thinking off",
|
|
43
|
+
* which a model that always thinks refuses. Undefined when nothing is left to
|
|
44
|
+
* drop, so the caller reports the error. */
|
|
45
|
+
export function withoutRefusedPart(request, detail, schemaPattern) {
|
|
46
|
+
if (request.responseFormat && schemaPattern.test(detail)) {
|
|
47
|
+
const { responseFormat: _dropped, ...rest } = request;
|
|
48
|
+
return rest;
|
|
49
|
+
}
|
|
50
|
+
if (request.reasoningEffort === "none" &&
|
|
51
|
+
/reasoning|effort|thinking/i.test(detail)) {
|
|
52
|
+
const { reasoningEffort: _dropped, ...rest } = request;
|
|
53
|
+
return rest;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
/** The model stopped at its output limit before writing a word. Thinking counts
|
|
58
|
+
* toward that limit, so a thinking model can spend all of it there. Without
|
|
59
|
+
* this the empty answer read as a normal one and failed later as "not JSON". */
|
|
60
|
+
export function outputLimitError(provider, model) {
|
|
61
|
+
return new Error(`${provider} model ${model} reached its output limit before writing an answer. Thinking counts toward that limit.`);
|
|
62
|
+
}
|
|
41
63
|
export function parseChatResponse(json, provider, model) {
|
|
42
64
|
const choice = Array.isArray(json.choices)
|
|
43
65
|
? json.choices[0]
|
|
@@ -61,8 +83,14 @@ export function parseChatResponse(json, provider, model) {
|
|
|
61
83
|
})
|
|
62
84
|
.filter((call) => call.id && call.function.name)
|
|
63
85
|
: undefined;
|
|
86
|
+
const text = String(message?.content ?? "");
|
|
87
|
+
if (choice?.finish_reason === "length" &&
|
|
88
|
+
!text.trim() &&
|
|
89
|
+
!toolCalls?.length) {
|
|
90
|
+
throw outputLimitError(provider, model);
|
|
91
|
+
}
|
|
64
92
|
return {
|
|
65
|
-
text
|
|
93
|
+
text,
|
|
66
94
|
...(toolCalls?.length ? { toolCalls } : {}),
|
|
67
95
|
tokensIn: Number(usage?.prompt_tokens ?? 0),
|
|
68
96
|
tokensOut: Number(usage?.completion_tokens ?? 0),
|
|
@@ -157,6 +157,9 @@ function extractRequest(prompt, maxTokens) {
|
|
|
157
157
|
job: "extract_unit",
|
|
158
158
|
prompt,
|
|
159
159
|
maxTokens,
|
|
160
|
+
// Pulling facts out of a text needs no thinking, and on a thinking model
|
|
161
|
+
// the thinking used the whole limit and left the answer empty.
|
|
162
|
+
reasoningEffort: "none",
|
|
160
163
|
system: "Extract only repository-specific facts. Never invent files, commands, or process.",
|
|
161
164
|
};
|
|
162
165
|
}
|