Normalize fetched model types with AgentKit 0.6.0

This commit is contained in:
Kaede Akino committed 2026-09-09 17:14:19 +08:00
1 parent d2f3a80d8d
commit 2afc70a285
4 files changed
+53 -9

No files matched your search

+7
View File
@@ -155,6 +155,13 @@ adapters at the LLM boundary. Models without tool capability receive no tools.
Stop cancels the active stream and actions that have not started; steering
messages are consumed at the next turn boundary.
Model purposes are `chat`, `embedding`, `rerank`, `image_generation`, and
`video_generation`. Only recognised purpose values from model discovery are
asserted as reported metadata. Unknown values, including Anthropic's `model`
object discriminator, leave catalog and name inference available and otherwise
default to chat. Only chat models enter conversation pickers. Persisted unknown
purpose strings also default to chat; there are no legacy purpose aliases.
A turn's tool calls run concurrently only when every call in it is marked
parallel; one sequential call makes the whole turn serial rather than being
fenced into its own segment. Concurrency is declared per tool and may be
+1 -1
View File
@@ -675,7 +675,7 @@
repositoryURL = "https://github.com/AkinoKaede/AgentKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.5.0;
minimumVersion = 0.6.0;
};
};
/* End XCRemoteSwiftPackageReference section */
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/AkinoKaede/AgentKit.git",
"state" : {
"revision" : "a7345ee68fd92b52589cecfc812517c089f39368",
"version" : "0.5.0"
"revision" : "ad0ab2f4f707657611faa671d41f229ff019d4e8",
"version" : "0.6.0"
}
},
{
+43 -6
View File
@@ -336,6 +336,32 @@ struct AIProviderPersistenceTests {
#expect(store.resolve(conversation: nil)?.model.id == hidden.id)
}
@Test(arguments: ["image", "other", "unknown-purpose"])
func unrecognisedStoredModelKindsLoadAsChat(kind: String) throws {
let container = try makeContainer("UnknownModelKind-" + kind)
let provider = ModelProvider(name: "Gateway", models: [AIModel(id: "private-chat")])
let store = AIConfigurationModel(modelContext: container.mainContext, persistsSecrets: false)
store.upsert(provider)
let row = try #require(container.mainContext.fetch(FetchDescriptor<PersistentAIModel>()).first)
row.kind = kind
try container.mainContext.save()
let reloaded = AIConfigurationModel(modelContext: container.mainContext, persistsSecrets: false)
let restored = try #require(reloaded.modelProvider(provider.id))
#expect(restored.models.first?.kind == .chat)
#expect(restored.assistantModels.map(\.id) == ["private-chat"])
}
@Test(arguments: AIModel.Kind.allCases)
func modelPurposesRoundTripThroughPersistence(kind: AIModel.Kind) throws {
let container = try makeContainer("ModelPurpose-" + kind.rawValue)
let provider = ModelProvider(name: "Gateway", models: [AIModel(id: "custom", kind: kind)])
let store = AIConfigurationModel(modelContext: container.mainContext, persistsSecrets: false)
store.upsert(provider)
let reloaded = AIConfigurationModel(modelContext: container.mainContext, persistsSecrets: false)
#expect(reloaded.modelProvider(provider.id)?.models.first?.kind == kind)
}
private func makeContainer(_ name: String) throws -> ModelContainer {
let configuration = localModelConfiguration(
name, schema: HostPersistence.schema, isStoredInMemoryOnly: true
@@ -368,6 +394,17 @@ private enum TestPersistenceError: Error {
@Suite
struct ModelCapabilityResolverTests {
@Test(arguments: ["model", "unknown-purpose", "", " \n"])
func unrecognisedWireTypeAllowsCatalogClassification(type: String) throws {
let data = try JSONSerialization.data(withJSONObject: [
"data": [["id": "gpt-5.4", "type": type]]
])
let model = try #require(ModelCatalogClient.parseOpenAIPage(data).models.first)
#expect(model.kind == .chat)
#expect(model.contextLength == 272_000)
#expect(model.abilities.contains(.toolCall))
}
@Test
func bundledPiCatalogEnrichesKnownModelWithoutRegex() {
let provider = ModelProvider(name: "OpenAI")
@@ -376,8 +413,8 @@ struct ModelCapabilityResolverTests {
provider: provider
)
#expect(ModelCapabilityResolver.catalogMetadata?.version == "0.84.3")
#expect(ModelCapabilityResolver.catalogMetadata?.modelCount == 1_312)
#expect(ModelCapabilityResolver.catalogMetadata?.version == "0.85.1")
#expect(ModelCapabilityResolver.catalogMetadata?.modelCount == 1_354)
#expect(resolution.catalogSource == .init(provider: "openai", modelID: "gpt-5.4"))
#expect(resolution.model.input == [.text, .image])
// `webSearch` is the one ability that does come from the model id, and
@@ -542,7 +579,7 @@ struct ModelCapabilityResolverTests {
func reportedFieldsWinOverCodexAutoReviewOverride() {
let provider = ModelProvider(name: "OpenAI")
var model = AIModel(id: "codex-auto-review")
model.kind = .other
model.kind = .embedding
model.input = [.audio]
model.output = [.image]
model.abilities = []
@@ -555,7 +592,7 @@ struct ModelCapabilityResolverTests {
reported: [.kind, .input, .output, .abilities]
)
#expect(resolution.model.kind == .other)
#expect(resolution.model.kind == .embedding)
#expect(resolution.model.input == [.audio])
#expect(resolution.model.output == [.image])
#expect(resolution.model.abilities.isEmpty)
@@ -568,7 +605,7 @@ struct ModelCapabilityResolverTests {
func explicitlyReportedFieldsWinOverPiCatalog() {
let provider = ModelProvider(name: "OpenAI")
var model = AIModel(id: "gpt-5.4")
model.kind = .other
model.kind = .embedding
model.input = [.audio]
model.output = [.image]
model.abilities = []
@@ -581,7 +618,7 @@ struct ModelCapabilityResolverTests {
reported: [.kind, .input, .output, .abilities]
)
#expect(resolution.model.kind == .other)
#expect(resolution.model.kind == .embedding)
#expect(resolution.model.input == [.audio])
#expect(resolution.model.output == [.image])
// Reported abilities still win — `toolCall` and `reasoning` stay off