mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
refactor(ai): keep last-one-wins for colliding tool names
This commit is contained in:
@@ -581,7 +581,7 @@ const lowerRequest = (request: LLMRequest) =>
|
||||
ToolNames.lower(prepareRequest(request), request.model.route.namespaces ?? "flat")
|
||||
|
||||
const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest, options?: StreamOptions) {
|
||||
const lowered = yield* lowerRequest(request)
|
||||
const lowered = lowerRequest(request)
|
||||
const resolved = lowered.request
|
||||
const route = resolved.model.route
|
||||
|
||||
@@ -683,15 +683,13 @@ export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer
|
||||
options: TriggerCompactOptions,
|
||||
): Effect.Effect<CompactionCheckpointResponse, AIError>
|
||||
function compact(request: LLMRequest, options?: EndpointCompactOptions | TriggerCompactOptions) {
|
||||
return lowerRequest(request).pipe(
|
||||
Effect.flatMap((lowered): Effect.Effect<CompactionResponse | CompactionCheckpointResponse, AIError> => {
|
||||
if (options?.mechanism === "trigger" && canCompact(request, options))
|
||||
return request.model.route.compact.trigger(lowered.request, executor, options)
|
||||
if ((options?.mechanism === undefined || options.mechanism === "endpoint") && canCompact(request))
|
||||
return request.model.route.compact.endpoint(lowered.request, executor, options)
|
||||
return unsupportedCompaction(request, options?.mechanism)
|
||||
}),
|
||||
)
|
||||
return Effect.suspend((): Effect.Effect<CompactionResponse | CompactionCheckpointResponse, AIError> => {
|
||||
if (options?.mechanism === "trigger" && canCompact(request, options))
|
||||
return request.model.route.compact.trigger(lowerRequest(request).request, executor, options)
|
||||
if ((options?.mechanism === undefined || options.mechanism === "endpoint") && canCompact(request))
|
||||
return request.model.route.compact.endpoint(lowerRequest(request).request, executor, options)
|
||||
return unsupportedCompaction(request, options?.mechanism)
|
||||
})
|
||||
}
|
||||
return Service.of({
|
||||
stream,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Effect } from "effect"
|
||||
import { LLMRequest, Message, ToolDefinition, type LLMEvent, type ToolEntry } from "./schema/index.js"
|
||||
import { ProviderShared } from "./protocols/shared.js"
|
||||
|
||||
/**
|
||||
* How a protocol receives tool namespaces. Callers always use the declared
|
||||
@@ -14,21 +12,17 @@ interface Name {
|
||||
readonly name: string
|
||||
}
|
||||
|
||||
/** Lower declared tool names to wire names; `raise` maps tool events back. */
|
||||
export const lower = Effect.fn("ToolNames.lower")(function* (request: LLMRequest, style: NamespaceStyle) {
|
||||
const names = new Map<string, Name>()
|
||||
for (const leaf of walk(request.tools)) {
|
||||
const bad = leaf.path.find((part) => part.includes("."))
|
||||
if (bad !== undefined) return yield* ProviderShared.invalidRequest(`Tool namespace "${bad}" must not contain "."`)
|
||||
const name = { namespace: leaf.path.join(".") || undefined, name: leaf.tool.name }
|
||||
const key = id(wire(name, style))
|
||||
const taken = names.get(key)
|
||||
if (taken)
|
||||
return yield* ProviderShared.invalidRequest(
|
||||
`Tools "${id(taken)}" and "${id(name)}" both use the provider tool name "${key}"`,
|
||||
)
|
||||
names.set(key, name)
|
||||
}
|
||||
/**
|
||||
* Lower declared tool names to wire names; `raise` maps tool events back.
|
||||
* Tools that share a wire name resolve last-one-wins in both directions.
|
||||
*/
|
||||
export const lower = (request: LLMRequest, style: NamespaceStyle) => {
|
||||
const names = new Map(
|
||||
walk(request.tools).map((leaf) => {
|
||||
const name = { namespace: leaf.path.join(".") || undefined, name: leaf.tool.name }
|
||||
return [id(wire(name, style)), name] as const
|
||||
}),
|
||||
)
|
||||
return {
|
||||
request: LLMRequest.update(request, {
|
||||
// Native namespaces are one level deep, so deeper levels join into the leaf name.
|
||||
@@ -50,7 +44,7 @@ export const lower = Effect.fn("ToolNames.lower")(function* (request: LLMRequest
|
||||
return name ? { ...event, ...name } : event
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const walk = (
|
||||
tools: ReadonlyArray<ToolEntry>,
|
||||
@@ -58,10 +52,14 @@ const walk = (
|
||||
): Array<{ path: ReadonlyArray<string>; tool: ToolDefinition }> =>
|
||||
tools.flatMap((tool) => (tool.type === "tool" ? [{ path, tool }] : walk(tool.tools, [...path, tool.name])))
|
||||
|
||||
const flatten = (tools: ReadonlyArray<ToolEntry>) =>
|
||||
walk(tools).map((leaf) =>
|
||||
leaf.path.length ? new ToolDefinition({ ...leaf.tool, name: [...leaf.path, leaf.tool.name].join("_") }) : leaf.tool,
|
||||
)
|
||||
const flatten = (tools: ReadonlyArray<ToolEntry>) => [
|
||||
...new Map(
|
||||
walk(tools).map((leaf) => {
|
||||
const name = [...leaf.path, leaf.tool.name].join("_")
|
||||
return [name, leaf.path.length ? new ToolDefinition({ ...leaf.tool, name }) : leaf.tool] as const
|
||||
}),
|
||||
).values(),
|
||||
]
|
||||
|
||||
const wire = (tool: Name, style: NamespaceStyle): Name => {
|
||||
const path = tool.namespace?.split(".") ?? []
|
||||
|
||||
@@ -213,23 +213,6 @@ describe("Open Responses-compatible route", () => {
|
||||
strict: false,
|
||||
},
|
||||
])
|
||||
|
||||
const collision = yield* compileRequest(
|
||||
LLM.request({
|
||||
model,
|
||||
tools: [
|
||||
ToolDefinition.make({ name: "acme_users", description: "Top-level users", inputSchema: {} }),
|
||||
{
|
||||
type: "namespace",
|
||||
name: "acme",
|
||||
tools: [ToolDefinition.make({ name: "users", description: "Lookup users", inputSchema: {} })],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).pipe(Effect.flip)
|
||||
expect(collision.message).toContain(
|
||||
'Tools "acme_users" and "acme.users" both use the provider tool name "acme_users"',
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user