mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 02:49:57 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1615dfca2a | |||
| 0030028ebc |
@@ -49,7 +49,7 @@ export const Model = Schema.Struct({
|
|||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
family: Schema.optional(Schema.String),
|
family: Schema.optional(Schema.String),
|
||||||
release_date: Schema.String,
|
release_date: Schema.String,
|
||||||
attachment: Schema.Boolean,
|
attachment: Schema.optional(Schema.Boolean),
|
||||||
reasoning: Schema.Boolean,
|
reasoning: Schema.Boolean,
|
||||||
temperature: Schema.Boolean,
|
temperature: Schema.Boolean,
|
||||||
tool_call: Schema.Boolean,
|
tool_call: Schema.Boolean,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { define } from "./internal"
|
|||||||
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
|
import { ModelV2 } from "../model"
|
||||||
import { ModelsDev } from "../models-dev"
|
import { ModelsDev } from "../models-dev"
|
||||||
import { ProviderV2 } from "../provider"
|
import { ProviderV2 } from "../provider"
|
||||||
|
|
||||||
@@ -97,11 +98,12 @@ function applyModel(
|
|||||||
url: model.provider?.api,
|
url: model.provider?.api,
|
||||||
settings: {},
|
settings: {},
|
||||||
}
|
}
|
||||||
draft.capabilities = {
|
const capabilities = ModelV2.Capabilities.defaults({
|
||||||
tools: model.tool_call,
|
tools: model.tool_call,
|
||||||
input: [...(model.modalities?.input ?? [])],
|
input: model.modalities?.input ?? (model.attachment === false ? ["text"] : undefined),
|
||||||
output: [...(model.modalities?.output ?? [])],
|
output: model.modalities?.output,
|
||||||
}
|
})
|
||||||
|
draft.capabilities = { ...capabilities, input: [...capabilities.input], output: [...capabilities.output] }
|
||||||
draft.variants = []
|
draft.variants = []
|
||||||
draft.time.released = released(model.release_date)
|
draft.time.released = released(model.release_date)
|
||||||
draft.cost = input.cost ?? cost(model.cost)
|
draft.cost = input.cost ?? cost(model.cost)
|
||||||
|
|||||||
@@ -10,13 +10,12 @@ import {
|
|||||||
} from "@opencode-ai/llm"
|
} from "@opencode-ai/llm"
|
||||||
import { Cause, DateTime, Effect, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
|
import { Cause, DateTime, Effect, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
|
||||||
import { AgentV2 } from "../../agent"
|
import { AgentV2 } from "../../agent"
|
||||||
|
import { Catalog } from "../../catalog"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { Database } from "../../database/database"
|
import { Database } from "../../database/database"
|
||||||
import { EventV2 } from "../../event"
|
import { EventV2 } from "../../event"
|
||||||
import { Location } from "../../location"
|
import { Location } from "../../location"
|
||||||
import { ModelV2 } from "../../model"
|
|
||||||
import { PermissionV2 } from "../../permission"
|
import { PermissionV2 } from "../../permission"
|
||||||
import { ProviderV2 } from "../../provider"
|
|
||||||
import { QuestionV2 } from "../../question"
|
import { QuestionV2 } from "../../question"
|
||||||
import { SystemContext } from "../../system-context/index"
|
import { SystemContext } from "../../system-context/index"
|
||||||
import { SystemContextRegistry } from "../../system-context/registry"
|
import { SystemContextRegistry } from "../../system-context/registry"
|
||||||
@@ -98,6 +97,7 @@ const layer = Layer.effect(
|
|||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
const tools = yield* ToolRegistry.Service
|
const tools = yield* ToolRegistry.Service
|
||||||
const models = yield* SessionRunnerModel.Service
|
const models = yield* SessionRunnerModel.Service
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
const store = yield* SessionStore.Service
|
const store = yield* SessionStore.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const systemContext = yield* SystemContextRegistry.Service
|
const systemContext = yield* SystemContextRegistry.Service
|
||||||
@@ -196,11 +196,21 @@ const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
const system =
|
const system =
|
||||||
initialized ?? (yield* SessionContextEpoch.prepare(db, events, loadSystemContext(agent), session.id))
|
initialized ?? (yield* SessionContextEpoch.prepare(db, events, loadSystemContext(agent), session.id))
|
||||||
const model = yield* models.resolve(session)
|
const resolved = yield* models.resolve(session)
|
||||||
|
const model = resolved.model
|
||||||
|
const catalogModel = yield* catalog.model.get(resolved.ref.providerID, resolved.ref.id)
|
||||||
|
if (!catalogModel)
|
||||||
|
return yield* new SessionRunnerModel.ModelUnavailableError({
|
||||||
|
providerID: resolved.ref.providerID,
|
||||||
|
modelID: resolved.ref.id,
|
||||||
|
})
|
||||||
const entries = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq)
|
const entries = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq)
|
||||||
const context = entries.map((entry) => entry.message)
|
const context = entries.map((entry) => entry.message)
|
||||||
const isLastStep = agent.info?.steps !== undefined && currentStep >= agent.info.steps
|
const isLastStep = agent.info?.steps !== undefined && currentStep >= agent.info.steps
|
||||||
const toolMaterialization = isLastStep ? undefined : yield* tools.materialize(agent.info?.permissions)
|
const toolMaterialization =
|
||||||
|
isLastStep || !catalogModel.capabilities.tools
|
||||||
|
? undefined
|
||||||
|
: yield* tools.materialize(agent.info?.permissions)
|
||||||
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
|
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
|
||||||
const request = LLM.request({
|
const request = LLM.request({
|
||||||
model,
|
model,
|
||||||
@@ -208,7 +218,10 @@ const layer = Layer.effect(
|
|||||||
system: [agent.info?.system, system.baseline]
|
system: [agent.info?.system, system.baseline]
|
||||||
.filter((part): part is string => part !== undefined && part.length > 0)
|
.filter((part): part is string => part !== undefined && part.length > 0)
|
||||||
.map(SystemPart.make),
|
.map(SystemPart.make),
|
||||||
messages: [...toLLMMessages(context, model), ...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : [])],
|
messages: [
|
||||||
|
...toLLMMessages(context, model, catalogModel.capabilities),
|
||||||
|
...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : []),
|
||||||
|
],
|
||||||
tools: toolMaterialization?.definitions ?? [],
|
tools: toolMaterialization?.definitions ?? [],
|
||||||
toolChoice: isLastStep ? "none" : undefined,
|
toolChoice: isLastStep ? "none" : undefined,
|
||||||
})
|
})
|
||||||
@@ -218,11 +231,7 @@ const layer = Layer.effect(
|
|||||||
const publisher = createLLMEventPublisher(events, {
|
const publisher = createLLMEventPublisher(events, {
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
agent: agent.id,
|
agent: agent.id,
|
||||||
model: {
|
model: resolved.ref,
|
||||||
id: ModelV2.ID.make(model.id),
|
|
||||||
providerID: ProviderV2.ID.make(model.provider),
|
|
||||||
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
|
|
||||||
},
|
|
||||||
snapshot: startSnapshot,
|
snapshot: startSnapshot,
|
||||||
})
|
})
|
||||||
const withPublication = Semaphore.makeUnsafe(1).withPermit
|
const withPublication = Semaphore.makeUnsafe(1).withPermit
|
||||||
@@ -420,6 +429,7 @@ export const node = makeLocationNode({
|
|||||||
AgentV2.node,
|
AgentV2.node,
|
||||||
ToolRegistry.node,
|
ToolRegistry.node,
|
||||||
SessionRunnerModel.node,
|
SessionRunnerModel.node,
|
||||||
|
Catalog.node,
|
||||||
SessionStore.node,
|
SessionStore.node,
|
||||||
Location.node,
|
Location.node,
|
||||||
SystemContextRegistry.node,
|
SystemContextRegistry.node,
|
||||||
|
|||||||
@@ -71,8 +71,15 @@ export type Error =
|
|||||||
| UnsupportedApiError
|
| UnsupportedApiError
|
||||||
| Integration.AuthorizationError
|
| Integration.AuthorizationError
|
||||||
|
|
||||||
|
export interface Resolved {
|
||||||
|
/** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */
|
||||||
|
readonly model: Model
|
||||||
|
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
|
||||||
|
readonly ref: ModelV2.Ref
|
||||||
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Model, Error>
|
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Resolved, Error>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRunnerModel") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRunnerModel") {}
|
||||||
@@ -80,6 +87,16 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||||||
/** Test or embedding seam for supplying a model resolver directly. */
|
/** Test or embedding seam for supplying a model resolver directly. */
|
||||||
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
|
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
|
||||||
|
|
||||||
|
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
|
||||||
|
export const resolved = (model: Model, variant?: ModelV2.VariantID): Resolved => ({
|
||||||
|
model,
|
||||||
|
ref: ModelV2.Ref.make({
|
||||||
|
id: ModelV2.ID.make(model.id),
|
||||||
|
providerID: ProviderV2.ID.make(model.provider),
|
||||||
|
...(variant === undefined ? {} : { variant }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
|
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
|
||||||
if (credential?.type === "key") return Auth.value(credential.key)
|
if (credential?.type === "key") return Auth.value(credential.key)
|
||||||
if (credential?.type === "oauth") return Auth.value(credential.access)
|
if (credential?.type === "oauth") return Auth.value(credential.access)
|
||||||
@@ -205,11 +222,19 @@ export const locationLayer = Layer.effect(
|
|||||||
const connection = yield* integrations.connection.active(
|
const connection = yield* integrations.connection.active(
|
||||||
provider?.integrationID ?? Integration.ID.make(selected.providerID),
|
provider?.integrationID ?? Integration.ID.make(selected.providerID),
|
||||||
)
|
)
|
||||||
return yield* resolve(
|
const model = yield* resolve(
|
||||||
session,
|
session,
|
||||||
selected,
|
selected,
|
||||||
connection ? yield* integrations.connection.resolve(connection) : undefined,
|
connection ? yield* integrations.connection.resolve(connection) : undefined,
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
|
model,
|
||||||
|
ref: ModelV2.Ref.make({
|
||||||
|
id: selected.id,
|
||||||
|
providerID: selected.providerID,
|
||||||
|
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
|
||||||
|
}),
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
type Model,
|
type Model,
|
||||||
type ProviderMetadata,
|
type ProviderMetadata,
|
||||||
} from "@opencode-ai/llm"
|
} from "@opencode-ai/llm"
|
||||||
|
import type { ModelV2 } from "../../model"
|
||||||
import { SessionMessage } from "../message"
|
import { SessionMessage } from "../message"
|
||||||
import type { FileAttachment } from "../prompt"
|
import type { FileAttachment } from "../prompt"
|
||||||
|
|
||||||
@@ -18,6 +19,23 @@ const media = (file: FileAttachment): ContentPart => ({
|
|||||||
metadata: file.description === undefined ? undefined : { description: file.description },
|
metadata: file.description === undefined ? undefined : { description: file.description },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const modality = (mime: string) => {
|
||||||
|
if (mime.startsWith("image/")) return "image"
|
||||||
|
if (mime.startsWith("audio/")) return "audio"
|
||||||
|
if (mime.startsWith("video/")) return "video"
|
||||||
|
if (mime === "application/pdf") return "pdf"
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = (file: FileAttachment, capabilities?: ModelV2.Capabilities): ContentPart => {
|
||||||
|
const type = modality(file.mime)
|
||||||
|
if (!type || (capabilities?.input ?? ["text", "image"]).includes(type)) return media(file)
|
||||||
|
return {
|
||||||
|
type: "text",
|
||||||
|
text: `ERROR: Cannot read ${file.name ? `"${file.name}"` : type} (this model does not support ${type} input). Inform the user.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toolInput = (tool: SessionMessage.AssistantTool) => {
|
const toolInput = (tool: SessionMessage.AssistantTool) => {
|
||||||
if (tool.state.status !== "pending") return tool.state.input
|
if (tool.state.status !== "pending") return tool.state.input
|
||||||
try {
|
try {
|
||||||
@@ -112,7 +130,11 @@ const assistant = (message: SessionMessage.Assistant, model: Model) => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function toLLMMessage(message: SessionMessage.Message, model: Model): Message[] {
|
function toLLMMessage(
|
||||||
|
message: SessionMessage.Message,
|
||||||
|
model: Model,
|
||||||
|
capabilities?: ModelV2.Capabilities,
|
||||||
|
): Message[] {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "agent-switched":
|
case "agent-switched":
|
||||||
case "model-switched":
|
case "model-switched":
|
||||||
@@ -122,7 +144,10 @@ function toLLMMessage(message: SessionMessage.Message, model: Model): Message[]
|
|||||||
Message.make({
|
Message.make({
|
||||||
id: message.id,
|
id: message.id,
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [{ type: "text", text: message.text }, ...(message.files ?? []).map(media)],
|
content: [
|
||||||
|
{ type: "text", text: message.text },
|
||||||
|
...(message.files ?? []).map((file) => attachment(file, capabilities)),
|
||||||
|
],
|
||||||
metadata: {
|
metadata: {
|
||||||
...message.metadata,
|
...message.metadata,
|
||||||
...(message.agents?.length ? { agents: message.agents } : {}),
|
...(message.agents?.length ? { agents: message.agents } : {}),
|
||||||
@@ -167,5 +192,8 @@ ${message.recent}
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Translate projected V2 Session history into canonical @opencode-ai/llm context. */
|
/** Translate projected V2 Session history into canonical @opencode-ai/llm context. */
|
||||||
export const toLLMMessages = (messages: readonly SessionMessage.Message[], model: Model) =>
|
export const toLLMMessages = (
|
||||||
messages.flatMap((message) => toLLMMessage(message, model))
|
messages: readonly SessionMessage.Message[],
|
||||||
|
model: Model,
|
||||||
|
capabilities?: ModelV2.Capabilities,
|
||||||
|
) => messages.flatMap((message) => toLLMMessage(message, model, capabilities))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ConfigMCPV1 } from "./mcp"
|
|||||||
import { ConfigPermissionV1 } from "./permission"
|
import { ConfigPermissionV1 } from "./permission"
|
||||||
import { ConfigProviderV1 } from "./provider"
|
import { ConfigProviderV1 } from "./provider"
|
||||||
import { ConfigProviderOptionsV1 } from "./provider-options"
|
import { ConfigProviderOptionsV1 } from "./provider-options"
|
||||||
|
import { ModelV2 } from "../../model"
|
||||||
|
|
||||||
const keys = new Set([
|
const keys = new Set([
|
||||||
"logLevel",
|
"logLevel",
|
||||||
@@ -211,8 +212,15 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
|
|||||||
: []),
|
: []),
|
||||||
]
|
]
|
||||||
const capabilities =
|
const capabilities =
|
||||||
info.tool_call !== undefined || info.modalities?.input !== undefined || info.modalities?.output !== undefined
|
info.tool_call !== undefined ||
|
||||||
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
|
info.attachment !== undefined ||
|
||||||
|
info.modalities?.input !== undefined ||
|
||||||
|
info.modalities?.output !== undefined
|
||||||
|
? ModelV2.Capabilities.defaults({
|
||||||
|
tools: info.tool_call,
|
||||||
|
input: info.modalities?.input ?? (info.attachment === false ? ["text"] : undefined),
|
||||||
|
output: info.modalities?.output,
|
||||||
|
})
|
||||||
: undefined
|
: undefined
|
||||||
return {
|
return {
|
||||||
family: info.family,
|
family: info.family,
|
||||||
|
|||||||
@@ -530,9 +530,17 @@ describe("Config", () => {
|
|||||||
options: { apiKey: "secret" },
|
options: { apiKey: "secret" },
|
||||||
models: {
|
models: {
|
||||||
model: {
|
model: {
|
||||||
|
attachment: true,
|
||||||
options: { reasoningEffort: "high" },
|
options: { reasoningEffort: "high" },
|
||||||
variants: { fast: { temperature: 0.2 } },
|
variants: { fast: { temperature: 0.2 } },
|
||||||
},
|
},
|
||||||
|
text: {
|
||||||
|
attachment: false,
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
attachment: true,
|
||||||
|
modalities: { input: ["audio"], output: ["audio"] },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openai: {
|
openai: {
|
||||||
@@ -608,9 +616,16 @@ describe("Config", () => {
|
|||||||
request: { body: { apiKey: "secret" } },
|
request: { body: { apiKey: "secret" } },
|
||||||
models: {
|
models: {
|
||||||
model: {
|
model: {
|
||||||
|
capabilities: { tools: false, input: ["text", "image"], output: ["text"] },
|
||||||
request: { body: { reasoningEffort: "high" } },
|
request: { body: { reasoningEffort: "high" } },
|
||||||
variants: [{ id: "fast", body: { temperature: 0.2 } }],
|
variants: [{ id: "fast", body: { temperature: 0.2 } }],
|
||||||
},
|
},
|
||||||
|
text: {
|
||||||
|
capabilities: { tools: false, input: ["text"], output: ["text"] },
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
capabilities: { tools: false, input: ["audio"], output: ["audio"] },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(documents[0]?.info.providers?.openai).toMatchObject({
|
expect(documents[0]?.info.providers?.openai).toMatchObject({
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||||||
|
|
||||||
const provider = required(yield* catalog.provider.get(providerID))
|
const provider = required(yield* catalog.provider.get(providerID))
|
||||||
const model = required(yield* catalog.model.get(providerID, modelID))
|
const model = required(yield* catalog.model.get(providerID, modelID))
|
||||||
|
const defaultModel = required(yield* catalog.model.get(providerID, ModelV2.ID.make("default")))
|
||||||
expect((yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default"))
|
expect((yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default"))
|
||||||
expect(provider.name).toBe("Renamed")
|
expect(provider.name).toBe("Renamed")
|
||||||
expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({
|
expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({
|
||||||
@@ -252,6 +253,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||||||
expect(model.api.id).toBe(ModelV2.ID.make("api-chat"))
|
expect(model.api.id).toBe(ModelV2.ID.make("api-chat"))
|
||||||
expect(model.name).toBe("Last")
|
expect(model.name).toBe("Last")
|
||||||
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
|
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
|
||||||
|
expect(defaultModel.capabilities).toEqual({ tools: false, input: ["text", "image"], output: ["text"] })
|
||||||
expect(model.enabled).toBe(false)
|
expect(model.enabled).toBe(false)
|
||||||
expect(model.limit).toEqual({ context: 100, output: 75 })
|
expect(model.limit).toEqual({ context: 100, output: 75 })
|
||||||
expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
|
expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, beforeAll, beforeEach, afterAll } from "bun:test"
|
import { describe, expect, beforeAll, beforeEach, afterAll } from "bun:test"
|
||||||
import { Effect, Layer, Ref } from "effect"
|
import { Effect, Layer, Ref, Schema } from "effect"
|
||||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
||||||
@@ -127,6 +127,21 @@ const initialState: MockState = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("ModelsDev Service", () => {
|
describe("ModelsDev Service", () => {
|
||||||
|
it.effect("allows models.dev entries without legacy attachment metadata", () =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
const result = Schema.decodeUnknownSync(ModelsDev.Model)({
|
||||||
|
id: "no-attachment-model",
|
||||||
|
name: "No Attachment Model",
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
reasoning: false,
|
||||||
|
temperature: true,
|
||||||
|
tool_call: true,
|
||||||
|
limit: { context: 128000, output: 8192 },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.attachment).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
it.live("get() returns providers from disk when cache file exists", () =>
|
it.live("get() returns providers from disk when cache file exists", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* writeCache(fixture)
|
yield* writeCache(fixture)
|
||||||
|
|||||||
@@ -76,6 +76,36 @@ describe("ModelsDevPlugin", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
default: {
|
||||||
|
id: "default",
|
||||||
|
name: "Default",
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
reasoning: false,
|
||||||
|
temperature: true,
|
||||||
|
tool_call: false,
|
||||||
|
limit: { context: 128_000, output: 8_192 },
|
||||||
|
},
|
||||||
|
explicit: {
|
||||||
|
id: "explicit",
|
||||||
|
name: "Explicit",
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
attachment: true,
|
||||||
|
reasoning: false,
|
||||||
|
temperature: true,
|
||||||
|
tool_call: false,
|
||||||
|
modalities: { input: ["audio"], output: ["audio"] },
|
||||||
|
limit: { context: 128_000, output: 8_192 },
|
||||||
|
},
|
||||||
|
legacy: {
|
||||||
|
id: "legacy",
|
||||||
|
name: "Legacy",
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
attachment: true,
|
||||||
|
reasoning: false,
|
||||||
|
temperature: true,
|
||||||
|
tool_call: true,
|
||||||
|
limit: { context: 128_000, output: 8_192 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} satisfies Record<string, ModelsDev.Provider>),
|
} satisfies Record<string, ModelsDev.Provider>),
|
||||||
@@ -92,9 +122,16 @@ describe("ModelsDevPlugin", () => {
|
|||||||
const providerID = ProviderV2.ID.make("acme")
|
const providerID = ProviderV2.ID.make("acme")
|
||||||
const base = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4"))
|
const base = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4"))
|
||||||
const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast"))
|
const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast"))
|
||||||
|
const defaults = yield* catalog.model.get(providerID, ModelV2.ID.make("default"))
|
||||||
|
const explicit = yield* catalog.model.get(providerID, ModelV2.ID.make("explicit"))
|
||||||
|
const legacy = yield* catalog.model.get(providerID, ModelV2.ID.make("legacy"))
|
||||||
|
|
||||||
expect(base?.variants).toEqual([])
|
expect(base?.variants).toEqual([])
|
||||||
expect(base?.request.body).toEqual({})
|
expect(base?.request.body).toEqual({})
|
||||||
|
expect(base?.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
|
||||||
|
expect(defaults?.capabilities).toEqual({ tools: false, input: ["text", "image"], output: ["text"] })
|
||||||
|
expect(explicit?.capabilities).toEqual({ tools: false, input: ["audio"], output: ["audio"] })
|
||||||
|
expect(legacy?.capabilities).toEqual({ tools: true, input: ["text", "image"], output: ["text"] })
|
||||||
expect(fast).toMatchObject({
|
expect(fast).toMatchObject({
|
||||||
id: "gpt-5.4-fast",
|
id: "gpt-5.4-fast",
|
||||||
providerID: "acme",
|
providerID: "acme",
|
||||||
|
|||||||
@@ -139,6 +139,145 @@ Recent work
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("defaults missing model capabilities to text and image input", () => {
|
||||||
|
const messages = toLLMMessages(
|
||||||
|
[
|
||||||
|
SessionMessage.User.make({
|
||||||
|
id: id("user-local-image"),
|
||||||
|
type: "user",
|
||||||
|
text: "Inspect this image",
|
||||||
|
files: [
|
||||||
|
FileAttachment.make({
|
||||||
|
uri: "data:image/png;base64,AAECAw==",
|
||||||
|
mime: "image/png",
|
||||||
|
name: "image.png",
|
||||||
|
}),
|
||||||
|
FileAttachment.make({
|
||||||
|
uri: "data:application/pdf;base64,JVBERg==",
|
||||||
|
mime: "application/pdf",
|
||||||
|
name: "document.pdf",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
time: { created },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
model,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(messages[0]?.content).toEqual([
|
||||||
|
{ type: "text", text: "Inspect this image" },
|
||||||
|
{ type: "media", mediaType: "image/png", data: "data:image/png;base64,AAECAw==", filename: "image.png" },
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: 'ERROR: Cannot read "document.pdf" (this model does not support pdf input). Inform the user.',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses explicit model input capabilities instead of attachment defaults", () => {
|
||||||
|
const messages = toLLMMessages(
|
||||||
|
[
|
||||||
|
SessionMessage.User.make({
|
||||||
|
id: id("user-unsupported-pdf"),
|
||||||
|
type: "user",
|
||||||
|
text: "Inspect these files",
|
||||||
|
files: [
|
||||||
|
FileAttachment.make({
|
||||||
|
uri: "data:image/png;base64,AAECAw==",
|
||||||
|
mime: "image/png",
|
||||||
|
name: "image.png",
|
||||||
|
}),
|
||||||
|
FileAttachment.make({
|
||||||
|
uri: "data:application/pdf;base64,JVBERg==",
|
||||||
|
mime: "application/pdf",
|
||||||
|
name: "document.pdf",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
time: { created },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
model,
|
||||||
|
{ tools: true, input: ["text", "pdf"], output: ["text"] },
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(messages[0]?.content).toEqual([
|
||||||
|
{ type: "text", text: "Inspect these files" },
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: 'ERROR: Cannot read "image.png" (this model does not support image input). Inform the user.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "media",
|
||||||
|
mediaType: "application/pdf",
|
||||||
|
data: "data:application/pdf;base64,JVBERg==",
|
||||||
|
filename: "document.pdf",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("treats explicit empty input capabilities as authoritative", () => {
|
||||||
|
const messages = toLLMMessages(
|
||||||
|
[
|
||||||
|
SessionMessage.User.make({
|
||||||
|
id: id("user-empty-capabilities"),
|
||||||
|
type: "user",
|
||||||
|
text: "Inspect this image",
|
||||||
|
files: [
|
||||||
|
FileAttachment.make({
|
||||||
|
uri: "data:image/png;base64,AAECAw==",
|
||||||
|
mime: "image/png",
|
||||||
|
name: "image.png",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
time: { created },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
model,
|
||||||
|
{ tools: false, input: [], output: [] },
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(messages[0]?.content).toEqual([
|
||||||
|
{ type: "text", text: "Inspect this image" },
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: 'ERROR: Cannot read "image.png" (this model does not support image input). Inform the user.',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("classifies audio and video MIME families through explicit capabilities", () => {
|
||||||
|
const messages = toLLMMessages(
|
||||||
|
[
|
||||||
|
SessionMessage.User.make({
|
||||||
|
id: id("user-media-capabilities"),
|
||||||
|
type: "user",
|
||||||
|
text: "Inspect this media",
|
||||||
|
files: [
|
||||||
|
FileAttachment.make({
|
||||||
|
uri: "data:audio/mpeg;base64,AAECAw==",
|
||||||
|
mime: "audio/mpeg",
|
||||||
|
name: "audio.mp3",
|
||||||
|
}),
|
||||||
|
FileAttachment.make({
|
||||||
|
uri: "data:video/webm;base64,AAECAw==",
|
||||||
|
mime: "video/webm",
|
||||||
|
name: "video.webm",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
time: { created },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
model,
|
||||||
|
{ tools: false, input: ["text", "audio", "video"], output: ["text"] },
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(messages[0]?.content).toEqual([
|
||||||
|
{ type: "text", text: "Inspect this media" },
|
||||||
|
{ type: "media", mediaType: "audio/mpeg", data: "data:audio/mpeg;base64,AAECAw==", filename: "audio.mp3" },
|
||||||
|
{ type: "media", mediaType: "video/webm", data: "data:video/webm;base64,AAECAw==", filename: "video.webm" },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
test("replays durable tool media into canonical tool messages without structured base64", () => {
|
test("replays durable tool media into canonical tool messages without structured base64", () => {
|
||||||
const messages = toLLMMessages(
|
const messages = toLLMMessages(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { EventV2 } from "@opencode-ai/core/event"
|
|||||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
import { Project } from "@opencode-ai/core/project"
|
import { Project } from "@opencode-ai/core/project"
|
||||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||||
@@ -32,6 +33,7 @@ import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry
|
|||||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||||
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
||||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||||
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
@@ -67,7 +69,21 @@ const model = OpenAIChat.route
|
|||||||
generation: { maxTokens: 20, temperature: 0 },
|
generation: { maxTokens: 20, temperature: 0 },
|
||||||
})
|
})
|
||||||
.model({ id: "gpt-4o-mini" })
|
.model({ id: "gpt-4o-mini" })
|
||||||
const models = SessionRunnerModel.layerWith(() => Effect.succeed(model))
|
const models = SessionRunnerModel.layerWith(() => Effect.succeed(SessionRunnerModel.resolved(model)))
|
||||||
|
const catalog = Layer.mock(Catalog.Service, {
|
||||||
|
provider: {
|
||||||
|
get: () => Effect.die("unused"),
|
||||||
|
all: () => Effect.die("unused"),
|
||||||
|
available: () => Effect.die("unused"),
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
get: (providerID, modelID) => Effect.succeed(ModelV2.Info.empty(providerID, modelID)),
|
||||||
|
all: () => Effect.die("unused"),
|
||||||
|
available: () => Effect.die("unused"),
|
||||||
|
default: () => Effect.die("unused"),
|
||||||
|
small: () => Effect.die("unused"),
|
||||||
|
},
|
||||||
|
})
|
||||||
const systemContext = AppNodeBuilder.build(SystemContextRegistry.node)
|
const systemContext = AppNodeBuilder.build(SystemContextRegistry.node)
|
||||||
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||||
@@ -76,6 +92,7 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
|
|||||||
[Snapshot.node, Snapshot.noopLayer],
|
[Snapshot.node, Snapshot.noopLayer],
|
||||||
[LayerNodePlatform.llmClient, client],
|
[LayerNodePlatform.llmClient, client],
|
||||||
[SessionRunnerModel.node, models],
|
[SessionRunnerModel.node, models],
|
||||||
|
[Catalog.node, catalog],
|
||||||
[SystemContextRegistry.node, systemContext],
|
[SystemContextRegistry.node, systemContext],
|
||||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||||
[SkillGuidance.node, skillGuidance],
|
[SkillGuidance.node, skillGuidance],
|
||||||
@@ -122,6 +139,7 @@ const it = testEffect(
|
|||||||
[PermissionV2.node, permission],
|
[PermissionV2.node, permission],
|
||||||
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
|
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
|
||||||
[SessionRunnerModel.node, models],
|
[SessionRunnerModel.node, models],
|
||||||
|
[Catalog.node, catalog],
|
||||||
[SystemContextRegistry.node, systemContext],
|
[SystemContextRegistry.node, systemContext],
|
||||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||||
[SkillGuidance.node, skillGuidance],
|
[SkillGuidance.node, skillGuidance],
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
|||||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
import { ConfigCompaction } from "@opencode-ai/core/config/compaction"
|
import { ConfigCompaction } from "@opencode-ai/core/config/compaction"
|
||||||
import { Tool } from "@opencode-ai/core/tool/tool"
|
import { Tool } from "@opencode-ai/core/tool/tool"
|
||||||
@@ -155,8 +156,33 @@ const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: ec
|
|||||||
let modelResolveHook = Effect.void
|
let modelResolveHook = Effect.void
|
||||||
let currentModel = model
|
let currentModel = model
|
||||||
const models = SessionRunnerModel.layerWith((session) =>
|
const models = SessionRunnerModel.layerWith((session) =>
|
||||||
modelResolveHook.pipe(Effect.as(session.model?.id === "replacement" ? replacementModel : currentModel)),
|
modelResolveHook.pipe(
|
||||||
|
Effect.as(
|
||||||
|
SessionRunnerModel.resolved(
|
||||||
|
session.model?.id === "replacement" ? replacementModel : currentModel,
|
||||||
|
session.model?.variant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
let catalogCapabilities = ModelV2.Capabilities.defaults({ tools: true })
|
||||||
|
const catalog = Layer.mock(Catalog.Service, {
|
||||||
|
provider: {
|
||||||
|
get: () => Effect.die("unused"),
|
||||||
|
all: () => Effect.die("unused"),
|
||||||
|
available: () => Effect.die("unused"),
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
get: (providerID, modelID) =>
|
||||||
|
Effect.succeed(
|
||||||
|
ModelV2.Info.make({ ...ModelV2.Info.empty(providerID, modelID), capabilities: catalogCapabilities }),
|
||||||
|
),
|
||||||
|
all: () => Effect.die("unused"),
|
||||||
|
available: () => Effect.die("unused"),
|
||||||
|
default: () => Effect.die("unused"),
|
||||||
|
small: () => Effect.die("unused"),
|
||||||
|
},
|
||||||
|
})
|
||||||
const systemContextKey = SystemContext.Key.make("test/context")
|
const systemContextKey = SystemContext.Key.make("test/context")
|
||||||
let systemBaseline = "Initial context"
|
let systemBaseline = "Initial context"
|
||||||
let systemRemoved = false
|
let systemRemoved = false
|
||||||
@@ -229,6 +255,7 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
|
|||||||
[Snapshot.node, Snapshot.noopLayer],
|
[Snapshot.node, Snapshot.noopLayer],
|
||||||
[LayerNodePlatform.llmClient, client],
|
[LayerNodePlatform.llmClient, client],
|
||||||
[SessionRunnerModel.node, models],
|
[SessionRunnerModel.node, models],
|
||||||
|
[Catalog.node, catalog],
|
||||||
[SystemContextRegistry.node, systemContext],
|
[SystemContextRegistry.node, systemContext],
|
||||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||||
[SkillGuidance.node, skillGuidance],
|
[SkillGuidance.node, skillGuidance],
|
||||||
@@ -278,6 +305,7 @@ const it = testEffect(
|
|||||||
[LayerNodePlatform.llmClient, client],
|
[LayerNodePlatform.llmClient, client],
|
||||||
[PermissionV2.node, permission],
|
[PermissionV2.node, permission],
|
||||||
[SessionRunnerModel.node, models],
|
[SessionRunnerModel.node, models],
|
||||||
|
[Catalog.node, catalog],
|
||||||
[SystemContextRegistry.node, systemContext],
|
[SystemContextRegistry.node, systemContext],
|
||||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||||
[SkillGuidance.node, skillGuidance],
|
[SkillGuidance.node, skillGuidance],
|
||||||
@@ -318,6 +346,7 @@ const setup = Effect.gen(function* () {
|
|||||||
systemLoadHook = Effect.void
|
systemLoadHook = Effect.void
|
||||||
modelResolveHook = Effect.void
|
modelResolveHook = Effect.void
|
||||||
currentModel = model
|
currentModel = model
|
||||||
|
catalogCapabilities = ModelV2.Capabilities.defaults({ tools: true })
|
||||||
skillBaselines.clear()
|
skillBaselines.clear()
|
||||||
responses = undefined
|
responses = undefined
|
||||||
streamFailure = undefined
|
streamFailure = undefined
|
||||||
@@ -655,6 +684,22 @@ describe("SessionRunnerLLM", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("does not advertise tools to a model without tool capability", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* setup
|
||||||
|
catalogCapabilities = ModelV2.Capabilities.defaults()
|
||||||
|
const session = yield* SessionV2.Service
|
||||||
|
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "No tools" }), resume: false })
|
||||||
|
|
||||||
|
requests.length = 0
|
||||||
|
response = []
|
||||||
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
|
expect(requests).toHaveLength(1)
|
||||||
|
expect(requests[0]?.tools).toEqual([])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("retries the first provider turn after system context becomes available", () =>
|
it.effect("retries the first provider turn after system context becomes available", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup
|
yield* setup
|
||||||
|
|||||||
@@ -26,7 +26,24 @@ export const Capabilities = Schema.Struct({
|
|||||||
tools: Schema.Boolean,
|
tools: Schema.Boolean,
|
||||||
input: Schema.Array(Schema.String),
|
input: Schema.Array(Schema.String),
|
||||||
output: Schema.Array(Schema.String),
|
output: Schema.Array(Schema.String),
|
||||||
}).annotate({ identifier: "Model.Capabilities" })
|
})
|
||||||
|
.annotate({ identifier: "Model.Capabilities" })
|
||||||
|
.pipe(
|
||||||
|
statics((schema) => ({
|
||||||
|
defaults: (
|
||||||
|
input: {
|
||||||
|
readonly tools?: boolean
|
||||||
|
readonly input?: ReadonlyArray<string>
|
||||||
|
readonly output?: ReadonlyArray<string>
|
||||||
|
} = {},
|
||||||
|
) =>
|
||||||
|
schema.make({
|
||||||
|
tools: input.tools ?? false,
|
||||||
|
input: input.input === undefined ? ["text", "image"] : [...input.input],
|
||||||
|
output: input.output === undefined ? ["text"] : [...input.output],
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
export interface Cost extends Schema.Schema.Type<typeof Cost> {}
|
export interface Cost extends Schema.Schema.Type<typeof Cost> {}
|
||||||
export const Cost = Schema.Struct({
|
export const Cost = Schema.Struct({
|
||||||
@@ -93,7 +110,7 @@ export const Info = Schema.Struct({
|
|||||||
providerID,
|
providerID,
|
||||||
name: modelID,
|
name: modelID,
|
||||||
api: { id: modelID, type: "native", settings: {} },
|
api: { id: modelID, type: "native", settings: {} },
|
||||||
capabilities: { tools: false, input: [], output: [] },
|
capabilities: Capabilities.defaults(),
|
||||||
request: { headers: {}, body: {} },
|
request: { headers: {}, body: {} },
|
||||||
variants: [],
|
variants: [],
|
||||||
time: { released: 0 },
|
time: { released: 0 },
|
||||||
|
|||||||
Reference in New Issue
Block a user