@buyong/pi-devin 0.1.1 → 0.1.3
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/package.json +1 -1
- package/src/core/inference/chat.ts +7 -2
- package/src/pi/messages.ts +26 -2
- package/src/pi/stream.ts +1 -1
package/package.json
CHANGED
|
@@ -60,6 +60,10 @@ export function chatRequest(options: {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Backends mark a finished turn differently: Anthropic text ends with MIN_LOG_PROB, Gemini tool calls with STOP_PATTERN,
|
|
64
|
+
// and OpenAI sends no stop reason (UNSPECIFIED is the proto3 default, so it is not on the wire).
|
|
65
|
+
const COMPLETE_STOP_REASONS = new Set(["STOP_REASON_STOP_PATTERN", "STOP_REASON_FUNCTION_CALL", "STOP_REASON_MIN_LOG_PROB", "STOP_REASON_UNSPECIFIED", ""]);
|
|
66
|
+
|
|
63
67
|
/** Attaches argument deltas without an ID to the call that started with the first ID. Ambiguous parallel deltas are rejected. */
|
|
64
68
|
export class ChatAccumulator {
|
|
65
69
|
readonly message: ChatMessage = { messageId: "", source: "CHAT_MESSAGE_SOURCE_SYSTEM", prompt: "" };
|
|
@@ -155,7 +159,7 @@ export class ChatAccumulator {
|
|
|
155
159
|
|
|
156
160
|
finish(): ChatResult {
|
|
157
161
|
if (!this.hasEnded) throw new Error("No successful EndStream.");
|
|
158
|
-
if (!
|
|
162
|
+
if (!COMPLETE_STOP_REASONS.has(this.stopReason) && this.stopReason !== "STOP_REASON_MAX_TOKENS") {
|
|
159
163
|
throw new Error(`stopReason cannot be treated as success: ${this.stopReason || "(none)"}`);
|
|
160
164
|
}
|
|
161
165
|
if (this.calls.size > 0) {
|
|
@@ -171,7 +175,8 @@ export class ChatAccumulator {
|
|
|
171
175
|
if (this.stopReason === "STOP_REASON_FUNCTION_CALL" && !this.calls.size) {
|
|
172
176
|
throw new Error("The response ended with FUNCTION_CALL but has no tool call.");
|
|
173
177
|
}
|
|
174
|
-
|
|
178
|
+
// Tool use is read from the calls themselves; only calls cut off at the output limit are rejected.
|
|
179
|
+
if (this.calls.size && !COMPLETE_STOP_REASONS.has(this.stopReason)) {
|
|
175
180
|
throw new Error("Tool calls and the stop reason do not match.");
|
|
176
181
|
}
|
|
177
182
|
this.message.messageId ||= randomUUID();
|
package/src/pi/messages.ts
CHANGED
|
@@ -40,14 +40,38 @@ export function attachReplayMetadata(output: AssistantMessage, message: ChatMess
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
const SCHEMA_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"]);
|
|
44
|
+
const SCHEMA_DATA = new Set(["enum", "const", "default", "examples", "example"]);
|
|
45
|
+
|
|
46
|
+
/** Devin's own tools give every schema a single `type`; its Gemini route rejects a list such as `["integer", "null"]`. */
|
|
47
|
+
function toDevinToolSchema(schema: unknown): unknown {
|
|
48
|
+
if (Array.isArray(schema)) return schema.map(toDevinToolSchema);
|
|
49
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
50
|
+
const result: Record<string, unknown> = {};
|
|
51
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
52
|
+
if (SCHEMA_DATA.has(key)) result[key] = value;
|
|
53
|
+
else if (SCHEMA_MAPS.has(key) && value && typeof value === "object" && !Array.isArray(value)) {
|
|
54
|
+
result[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, toDevinToolSchema(child)]));
|
|
55
|
+
} else if (key === "type" && Array.isArray(value)) {
|
|
56
|
+
// Omitting the field still covers null; several remaining types become one branch each.
|
|
57
|
+
const types = value.filter((type) => type !== "null");
|
|
58
|
+
if (types.length === 1) result.type = types[0];
|
|
59
|
+
else if (types.length > 1 && !("anyOf" in schema)) result.anyOf = types.map((type) => ({ type }));
|
|
60
|
+
else result.type = value;
|
|
61
|
+
} else result[key] = toDevinToolSchema(value);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
export function toDevinTranscript(context: TranscriptContext, model: ModelInfo): { messages: ChatMessage[]; tools: ChatTool[]; systemPrompt: string } {
|
|
44
67
|
const declarations = getCurrentTools(context.messages);
|
|
45
68
|
if (declarations.length && !model.supportsToolCalls) throw new Error("This model does not support the current Pi tool declarations.");
|
|
46
69
|
const tools = declarations.map((tool) => {
|
|
47
|
-
|
|
70
|
+
// Grammar sampling is optional; like Pi's providers without grammar tools, send the JSON schema instead.
|
|
71
|
+
if (tool.constrainedSampling && tool.constrainedSampling.type === "json_schema" && tool.constrainedSampling.strict === "require") {
|
|
48
72
|
throw new Error(`Devin does not support this tool's forced sampling format: ${tool.name}`);
|
|
49
73
|
}
|
|
50
|
-
return { name: tool.name, description: tool.description, jsonSchemaString: JSON.stringify(tool.parameters) };
|
|
74
|
+
return { name: tool.name, description: tool.description, jsonSchemaString: JSON.stringify(toDevinToolSchema(tool.parameters)) };
|
|
51
75
|
});
|
|
52
76
|
const failedCalls = new Set(context.messages.flatMap((message) => message.role === "assistant" && ["error", "aborted"].includes(message.stopReason)
|
|
53
77
|
? message.content.filter((block) => block.type === "toolCall").map((block) => block.id) : []));
|
package/src/pi/stream.ts
CHANGED
|
@@ -107,7 +107,7 @@ export class DevinResponseStream {
|
|
|
107
107
|
this.closeContent();
|
|
108
108
|
this.hasTerminated = true;
|
|
109
109
|
this.message.rawStopReason = result.stopReason;
|
|
110
|
-
this.message.stopReason = result.
|
|
110
|
+
this.message.stopReason = result.message.toolCalls?.length ? "toolUse" : result.stopReason === "STOP_REASON_MAX_TOKENS" ? "length" : "stop";
|
|
111
111
|
this.stream.push({ type: "done", reason: this.message.stopReason, message: this.message });
|
|
112
112
|
this.stream.end(this.message);
|
|
113
113
|
}
|