@buyong/pi-devin 0.1.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buyong/pi-devin",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Unofficial Pi provider for Devin accounts (OAuth)",
6
6
  "license": "MIT",
@@ -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 (!["STOP_REASON_STOP_PATTERN", "STOP_REASON_FUNCTION_CALL", "STOP_REASON_MAX_TOKENS"].includes(this.stopReason)) {
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
- if (this.calls.size && this.stopReason !== "STOP_REASON_FUNCTION_CALL") {
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();
@@ -40,6 +40,29 @@ 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.");
@@ -48,7 +71,7 @@ export function toDevinTranscript(context: TranscriptContext, model: ModelInfo):
48
71
  if (tool.constrainedSampling && tool.constrainedSampling.type === "json_schema" && tool.constrainedSampling.strict === "require") {
49
72
  throw new Error(`Devin does not support this tool's forced sampling format: ${tool.name}`);
50
73
  }
51
- 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)) };
52
75
  });
53
76
  const failedCalls = new Set(context.messages.flatMap((message) => message.role === "assistant" && ["error", "aborted"].includes(message.stopReason)
54
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.stopReason === "STOP_REASON_FUNCTION_CALL" ? "toolUse" : result.stopReason === "STOP_REASON_MAX_TOKENS" ? "length" : "stop";
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
  }