Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-openai-specialized-tool-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/ai-openai": patch
---

Emit specialized OpenAI tool results only once.
5 changes: 4 additions & 1 deletion packages/ai/openai/src/OpenAiLanguageModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,7 @@ const prepareMessages = Effect.fnUntraced(
Tool.isProviderDefined(tool) && tool.name === "OpenAiCodeInterpreter"
)
const shellTool = options.tools.find((tool): tool is ReturnType<typeof OpenAiTool.Shell> =>
Tool.isProviderDefined(tool) && tool.name === "OpenAiFunctionShell"
Tool.isProviderDefined(tool) && tool.name === "OpenAiShell"
)
const localShellTool = options.tools.find((tool): tool is ReturnType<typeof OpenAiTool.LocalShell> =>
Tool.isProviderDefined(tool) && tool.name === "OpenAiLocalShell"
Expand Down Expand Up @@ -1130,6 +1130,7 @@ const prepareMessages = Effect.fnUntraced(
call_id: part.id,
...(part.result as any)
})
continue
}

if (Predicate.isNotUndefined(shellTool) && toolName === "shell") {
Expand All @@ -1140,6 +1141,7 @@ const prepareMessages = Effect.fnUntraced(
output: part.result as any,
...(Predicate.isNotNull(status) ? { status } : {})
})
continue
}

if (Predicate.isNotUndefined(localShellTool) && toolName === "local_shell") {
Expand All @@ -1150,6 +1152,7 @@ const prepareMessages = Effect.fnUntraced(
output: part.result as any,
...(Predicate.isNotNull(status) ? { status } : {})
})
continue
}

messages.push({
Expand Down
135 changes: 135 additions & 0 deletions packages/ai/openai/test/OpenAiLanguageModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,141 @@ describe("OpenAiLanguageModel", () => {
strictEqual(toolOutput.call_id, "call_abc")
strictEqual(toolOutput.output, JSON.stringify({ output: "result" }))
}).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer])))

it.effect("emits only the specialized output for apply_patch results", () =>
Effect.gen(function*() {
const toolkit = Toolkit.make(OpenAiTool.ApplyPatch({}))
yield* LanguageModel.generateText({
prompt: Prompt.make([
{ role: "user", content: "Apply a patch" },
{
role: "assistant",
content: [Prompt.toolCallPart({
id: "call_apply_patch",
name: "OpenAiApplyPatch",
params: {
call_id: "call_apply_patch",
operation: { type: "delete_file", path: "old.ts" }
},
providerExecuted: false
})]
},
{
role: "tool",
content: [Prompt.toolResultPart({
id: "call_apply_patch",
name: "OpenAiApplyPatch",
isFailure: false,
result: { status: "completed", output: "deleted" }
})]
}
]),
toolkit,
disableToolCallResolution: true
}).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))

const requests = yield* MockHttpClient.requests
const body = yield* getRequestBody(requests[0])
const outputs = body.input.filter((item: any) =>
item.call_id === "call_apply_patch" && item.type.endsWith("_output")
)

deepStrictEqual(outputs.map((item: any) => item.type), ["apply_patch_call_output"])
}).pipe(Effect.provide(makeTestLayer())))

it.effect("emits only the specialized output for shell results", () =>
Effect.gen(function*() {
const toolkit = Toolkit.make(OpenAiTool.Shell({}))
yield* LanguageModel.generateText({
prompt: Prompt.make([
{ role: "user", content: "Run a shell command" },
{
role: "assistant",
content: [Prompt.toolCallPart({
id: "call_shell",
name: "OpenAiShell",
params: {
action: {
commands: ["echo hello"],
timeout_ms: null,
max_output_length: null
}
},
providerExecuted: false
})]
},
{
role: "tool",
content: [Prompt.toolResultPart({
id: "call_shell",
name: "OpenAiShell",
isFailure: false,
result: {
output: [{
stdout: "hello\n",
stderr: "",
outcome: { type: "exit", exit_code: 0 }
}]
}
})]
}
]),
toolkit,
disableToolCallResolution: true
}).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))

const requests = yield* MockHttpClient.requests
const body = yield* getRequestBody(requests[0])
const outputs = body.input.filter((item: any) =>
item.call_id === "call_shell" && item.type.endsWith("_output")
)

deepStrictEqual(outputs.map((item: any) => item.type), ["shell_call_output"])
}).pipe(Effect.provide(makeTestLayer())))

it.effect("emits only the specialized output for local_shell results", () =>
Effect.gen(function*() {
const toolkit = Toolkit.make(OpenAiTool.LocalShell({}))
yield* LanguageModel.generateText({
prompt: Prompt.make([
{ role: "user", content: "Run a local shell command" },
{
role: "assistant",
content: [Prompt.toolCallPart({
id: "call_local_shell",
name: "OpenAiLocalShell",
params: {
action: {
type: "exec",
command: ["echo", "hello"],
env: {}
}
},
providerExecuted: false
})]
},
{
role: "tool",
content: [Prompt.toolResultPart({
id: "call_local_shell",
name: "OpenAiLocalShell",
isFailure: false,
result: { output: "hello\n" }
})]
}
]),
toolkit,
disableToolCallResolution: true
}).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))

const requests = yield* MockHttpClient.requests
const body = yield* getRequestBody(requests[0])
const outputs = body.input.filter((item: any) =>
item.call_id === "call_local_shell" && item.type.endsWith("_output")
)

deepStrictEqual(outputs.map((item: any) => item.type), ["local_shell_call_output"])
}).pipe(Effect.provide(makeTestLayer())))
})
})

Expand Down
Loading