FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(schema): default omitted capability tools for custom providers by cestercian · Pull Request #49940 · anomalyco/opencode · GitHub

Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .ts  (6) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
38 changes: 28 additions & 10 deletions packages/core/src/config/normalize.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export * as ConfigNormalize from "./normalize.js"

import { isDeepStrictEqual } from "node:util"
import { isRecord } from "@opencode/ai/utils/record"
import { Option, Schema } from "effect"
import { Option, Result, Schema, SchemaIssue, SchemaParser } from "effect"
import { Info } from "@opencode/schema/config"
import { ConfigAgent } from "@opencode/schema/config/agent"
import { ConfigCommand } from "@opencode/schema/config/command"
Expand Down Expand Up @@ -674,9 +674,9 @@ function decodeValue<S extends Schema.Codec<unknown, unknown, never, never>>(
path: string[],
diagnostics: Diagnostic[],
) {
const decoded = Schema.decodeUnknownOption(schema, options)(value)
if (Option.isSome(decoded)) return decoded.value
invalid(path, diagnostics)
const decoded = SchemaParser.decodeUnknownResult(schema, options)(value)
if (Result.isSuccess(decoded)) return decoded.success
invalid(path, diagnostics, decoded.failure)
return undefined
}

Expand All @@ -686,12 +686,12 @@ function decodeEncoded<S extends Schema.Codec<unknown, unknown, never, never>>(
path: string[],
diagnostics: Diagnostic[],
) {
const decoded = Schema.decodeUnknownOption(schema, options)(value)
if (Option.isNone(decoded)) {
invalid(path, diagnostics)
const decoded = SchemaParser.decodeUnknownResult(schema, options)(value)
if (Result.isFailure(decoded)) {
invalid(path, diagnostics, decoded.failure)
return undefined
}
const encoded = Schema.encodeUnknownOption(schema, options)(decoded.value)
const encoded = Schema.encodeUnknownOption(schema, options)(decoded.success)
if (Option.isSome(encoded)) return plain(encoded.value)
invalid(path, diagnostics)
return undefined
Expand Down Expand Up @@ -773,8 +773,26 @@ function unsupportedIfPresent(value: Record<string, unknown>, key: string, path:
diagnostics.push({ kind: "unsupported", path, message: "omitted unsupported legacy setting" })
}

function invalid(path: string[], diagnostics: Diagnostic[]) {
diagnostics.push({ kind: "invalid", path, message: "skipped malformed recognized value" })
const formatIssue = SchemaIssue.makeFormatterStandardSchemaV1()

function invalid(path: string[], diagnostics: Diagnostic[], issue?: SchemaIssue.Issue) {
const field = issue ? firstIssuePath(issue) : []
diagnostics.push({
kind: "invalid",
path: field.length ? [...path, ...field] : path,
message: "skipped malformed recognized value",
})
}

function firstIssuePath(issue: SchemaIssue.Issue) {
const formatted = formatIssue(issue)
const path = formatted.issues?.[0]?.path
if (!path) return []
return path.flatMap((segment) => {
const key = typeof segment === "object" && segment !== null && "key" in segment ? segment.key : segment
if (typeof key === "string" || typeof key === "number") return [String(key)]
return []
})
}

function conflict(path: string[], diagnostics: Diagnostic[]) {
Expand Down
66 changes: 60 additions & 6 deletions packages/core/test/config/normalization.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,58 @@ describe("ConfigNormalize", () => {
expect(JSON.stringify(result.diagnostics)).not.toContain(secret)
})

test("keeps a custom provider whose model capabilities omit tools", () => {
const result = normalized({
providers: {
acme: {
package: "aisdk:@ai-sdk/openai-compatible",
settings: { apiKey: "{env:ACME_API_KEY}", baseURL: "https://llm.example.com/v1" },
models: {
coder: {
limit: { context: 262144, output: 32768 },
capabilities: { input: ["text", "image"], output: ["text"] },
},
},
},
},
})
expect(result.diagnostics).toEqual([])
expect(result.encoded.providers).toEqual({
acme: {
package: "aisdk:@ai-sdk/openai-compatible",
settings: { apiKey: "{env:ACME_API_KEY}", baseURL: "https://llm.example.com/v1" },
models: {
coder: {
limit: { context: 262144, output: 32768 },
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
},
},
},
})
})

test("names the offending field when a recognized provider value is malformed", () => {
const result = normalized({
providers: {
acme: {
models: {
coder: {
capabilities: { tools: "yes", input: ["text"], output: ["text"] },
},
},
},
},
})
expect(result.encoded.providers).toEqual({})
expect(result.diagnostics.filter((item) => item.kind === "invalid")).toEqual([
{
kind: "invalid",
path: ["providers", "acme", "models", "coder", "capabilities", "tools"],
message: "skipped malformed recognized value",
},
])
})

test("recovers malformed named entries and retains a valid legacy collision", () => {
const result = normalized({
command: { fallback: { template: "legacy" } },
Expand All @@ -197,9 +249,9 @@ describe("ConfigNormalize", () => {
expect(result.encoded.commands).toEqual({ fallback: { template: "legacy" }, valid: { template: "native" } })
expect(result.encoded.providers).toEqual({ valid: { name: "Valid" } })
expect(result.diagnostics.filter((item) => item.kind === "invalid").map((item) => item.path)).toEqual([
["commands", "fallback"],
["commands", "invalid"],
["providers", "invalid"],
["commands", "fallback", "template"],
["commands", "invalid", "template"],
["providers", "invalid", "env", "0"],
])
})

Expand All @@ -214,6 +266,8 @@ describe("ConfigNormalize", () => {
expect(result.diagnostics.filter((item) => item.kind === "invalid").map((item) => item.path)).toContainEqual([
"provider",
"azure",
"env",
"0",
])
})

Expand Down Expand Up @@ -292,8 +346,8 @@ describe("ConfigNormalize", () => {
expect(invalid.encoded).not.toHaveProperty("formatter")
expect(invalid.encoded).not.toHaveProperty("lsp")
expect(invalid.diagnostics.filter((item) => item.kind === "invalid").map((item) => item.path)).toEqual([
["formatter", "prettier"],
["lsp", "typescript"],
["formatter", "prettier", "command", "0"],
["lsp", "typescript", "command", "0"],
])

expect(normalized({ formatter: {}, lsp: {} }).encoded).toMatchObject({ formatter: {}, lsp: {} })
Expand Down Expand Up @@ -328,7 +382,7 @@ describe("ConfigNormalize", () => {
result.diagnostics.some((item) => item.kind === "conflict" && item.path.join(".") === "mcp.timeout.catalog"),
).toBe(true)
expect(
result.diagnostics.some((item) => item.kind === "invalid" && item.path.join(".") === "mcp.servers.invalid"),
result.diagnostics.some((item) => item.kind === "invalid" && item.path.join(".") === "mcp.servers.invalid.command.0"),
).toBe(true)
})

Expand Down
30 changes: 30 additions & 0 deletions packages/core/test/config/provider.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,36 @@ describe("ConfigProviderPlugin.Plugin", () => {
}),
)

it.effect("defaults omitted capability tools for configured models", () =>
Effect.gen(function* () {
const models = yield* Model.Service
const providerID = Provider.ID.make("acme")
const modelID = Model.ID.make("coder")
yield* addPlugin([
new Document({
type: "document",
info: decode({
providers: {
acme: {
package: "aisdk:@ai-sdk/openai-compatible",
models: {
coder: {
limit: { context: 262144, output: 32768 },
capabilities: { input: ["text", "image"], output: ["text"] },
},
},
},
},
}),
}),
])

const model = required(yield* models.get(providerID, modelID))
expect(model.capabilities).toEqual({ tools: true, input: ["text", "image"], output: ["text"] })
expect(model.limit).toEqual({ context: 262144, output: 32768 })
}),
)

it.effect("defaults custom model metadata", () =>
Effect.gen(function* () {
const models = yield* Model.Service
Expand Down
5 changes: 3 additions & 2 deletions packages/schema/src/model.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * as Model from "./model.js"

import { Schema } from "effect"
import { Effect, Schema } from "effect"
import { optional, statics } from "./schema.js"
import { Provider } from "./provider.js"
import { Money } from "./money.js"
Expand Down Expand Up @@ -84,7 +84,8 @@ export const Compatibility = Schema.Struct({

export interface Capabilities extends Schema.Schema.Type<typeof Capabilities> {}
export const Capabilities = Schema.Struct({
tools: Schema.Boolean,
// Unknown models assume tool support; omitted tools must not fail decode.
tools: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
input: Schema.Array(Schema.String),
output: Schema.Array(Schema.String),
})
Expand Down
21 changes: 21 additions & 0 deletions packages/schema/test/config.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@ describe("Config.Entry", () => {
expect(() => decode({ worktree: { directory: " " } })).toThrow()
expect(() => decode({ worktree: { directory: false } })).toThrow()
})
test("defaults omitted model capability tools for custom providers", () => {
const decoded = Schema.decodeUnknownSync(Config.Info)({
providers: {
acme: {
package: "aisdk:@ai-sdk/openai-compatible",
models: {
coder: {
limit: { context: 262144, output: 32768 },
capabilities: { input: ["text", "image"], output: ["text"] },
},
},
},
},
})
expect(decoded.providers?.acme?.models?.coder?.capabilities).toEqual({
tools: true,
input: ["text", "image"],
output: ["text"],
})
})

test("round-trips canonical provider IDs without changing config keys", () => {
const input = { providers: { "console-anthropic": { canonical: "anthropic" } } }
const decoded = Schema.decodeUnknownSync(Config.Info)(input)
Expand Down
18 changes: 18 additions & 0 deletions packages/schema/test/model.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,21 @@ describe("Model.Settings", () => {
})
})
})

describe("Model.Capabilities", () => {
test("defaults omitted tools to the unknown-model assumption", () => {
const decode = Schema.decodeUnknownSync(Model.Capabilities)

expect(decode({ input: ["text", "image"], output: ["text"] })).toEqual({
tools: true,
input: ["text", "image"],
output: ["text"],
})
expect(decode({ tools: false, input: ["text"], output: ["text"] })).toEqual({
tools: false,
input: ["text"],
output: ["text"],
})
expect(() => decode({ tools: "yes", input: ["text"], output: ["text"] })).toThrow()
})
})
Loading

Back | FazBrowse Home | New Git URL