From ac44585a431689b42bd4c89079a9a234338bce40 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 28 Oct 2026 13:02:47 +0800 Subject: [PATCH 01/10] feat: add Jev decision model support --- README.md | 1 + README_EN.md | 1 + api-docs/openapi/v3_0/aiFoundationApis.json | 219 ++++++++++++++++-- .../run/halo/aifoundation/AiModelService.java | 14 ++ .../capability/CapabilityDomain.java | 5 +- .../capability/DecisionCapability.java | 20 ++ .../capability/ModelCapabilities.java | 9 + .../ModelCapabilityRequirement.java | 8 + .../capability/ModelCapabilitySources.java | 9 + .../aifoundation/decision/DecisionAnswer.java | 28 +++ .../aifoundation/decision/DecisionModel.java | 14 ++ .../decision/DecisionQuestion.java | 40 ++++ .../decision/DecisionQuestionType.java | 8 + .../decision/DecisionRequest.java | 35 +++ .../decision/DecisionResponse.java | 18 ++ .../aifoundation/decision/DecisionUsage.java | 16 ++ .../exception/DecisionCancelledException.java | 8 + .../exception/DecisionTimeoutException.java | 17 ++ .../endpoint/DecisionConsoleEndpoint.java | 99 ++++++++ .../DefaultModelSlotConsoleEndpoint.java | 1 + .../endpoint/ModelOptionQuery.java | 28 ++- .../aifoundation/provider/AiProviderType.java | 11 + .../provider/support/AdapterProtocol.java | 3 +- .../provider/support/AdapterType.java | 3 +- .../provider/support/ModelType.java | 3 +- .../provider/support/ProviderClientCache.java | 12 + .../support/ProviderDecisionClient.java | 11 + .../support/ProviderModelResolver.java | 1 + .../typesafe/TypeSafeDecisionClient.java | 161 +++++++++++++ .../provider/typesafe/TypeSafeProvider.java | 131 +++++++++++ .../aifoundation/service/AiModelResolver.java | 2 + .../service/AiModelServiceImpl.java | 19 +- .../service/audit/AuditedDecisionModel.java | 31 +++ .../capability/ModelCapabilityMatcher.java | 22 ++ .../capability/ModelCapabilityService.java | 5 + .../decision/DecisionModelFactory.java | 26 +++ .../service/decision/DecisionModelImpl.java | 67 ++++++ .../service/decision/DecisionValidation.java | 175 ++++++++++++++ .../service/model/DefaultAiModelResolver.java | 5 + .../service/observation/NormalizedUsage.java | 7 + .../service/observation/UsageError.java | 4 + .../service/observation/UsageOperation.java | 1 + .../service/observation/UsageUnitKind.java | 3 +- .../setting/DefaultModelSlots.java | 2 + .../endpoint/DecisionConsoleEndpointTest.java | 48 ++++ .../ModelOptionConsoleEndpointTest.java | 29 +++ .../typesafe/TypeSafeDecisionClientTest.java | 143 ++++++++++++ .../service/AiModelServiceImplTest.java | 44 +++- .../audit/AuditedDecisionModelTest.java | 76 ++++++ .../service/decision/DecisionModelTest.java | 98 ++++++++ dev/en/README.md | 2 +- dev/en/model-selector.md | 2 +- dev/en/sdk-core/README.md | 3 +- dev/en/sdk-core/api-reference.md | 13 +- dev/en/sdk-core/decision-models.md | 83 +++++++ dev/zh-CN/dev.md | 4 +- dev/zh-CN/model-selector.md | 2 +- dev/zh-CN/sdk-core/README.md | 3 +- dev/zh-CN/sdk-core/api-reference.md | 17 +- dev/zh-CN/sdk-core/decision-models.md | 97 ++++++++ ui/packages/sdk/README.md | 7 + ui/packages/sdk/README.zh-CN.md | 6 + ui/packages/sdk/src/decision.test.ts | 94 ++++++++ ui/packages/sdk/src/decision.ts | 166 +++++++++++++ ui/packages/sdk/src/index.ts | 12 +- ui/src/api/generated/.openapi-generator/FILES | 6 + ui/src/api/generated/api.ts | 2 +- .../generated/api/ai-model-v1alpha1-api.ts | 2 +- .../generated/api/ai-provider-v1alpha1-api.ts | 2 +- ...ion-halo-run-v1alpha1-caller-plugin-api.ts | 2 +- ...alo-run-v1alpha1-default-model-slot-api.ts | 2 +- ...ifoundation-halo-run-v1alpha1-model-api.ts | 107 ++++++++- ...tion-halo-run-v1alpha1-model-option-api.ts | 2 +- ...undation-halo-run-v1alpha1-provider-api.ts | 2 +- ...ion-halo-run-v1alpha1-provider-type-api.ts | 2 +- ...-halo-run-v1alpha1-usage-statistics-api.ts | 2 +- ui/src/api/generated/base.ts | 2 +- ui/src/api/generated/common.ts | 2 +- ui/src/api/generated/configuration.ts | 2 +- ui/src/api/generated/index.ts | 2 +- .../api/generated/models/adapter-type-info.ts | 8 +- ui/src/api/generated/models/add-operation.ts | 2 +- ui/src/api/generated/models/ai-model-list.ts | 2 +- ui/src/api/generated/models/ai-model-spec.ts | 8 +- ui/src/api/generated/models/ai-model.ts | 2 +- .../api/generated/models/ai-provider-list.ts | 2 +- .../api/generated/models/ai-provider-spec.ts | 2 +- .../generated/models/ai-provider-status.ts | 2 +- ui/src/api/generated/models/ai-provider.ts | 2 +- .../generated/models/caller-plugin-info.ts | 2 +- ui/src/api/generated/models/copy-operation.ts | 2 +- ui/src/api/generated/models/data-content.ts | 2 +- .../api/generated/models/decision-answer.ts | 75 ++++++ .../generated/models/decision-capability.ts | 57 +++++ .../api/generated/models/decision-question.ts | 69 ++++++ .../api/generated/models/decision-response.ts | 48 ++++ ui/src/api/generated/models/decision-usage.ts | 36 +++ .../generated/models/default-model-slots.ts | 8 +- .../models/default-parameter-mapping-info.ts | 2 +- .../generated/models/discovered-model-item.ts | 8 +- .../generated/models/embedding-mappings.ts | 2 +- .../api/generated/models/embedding-preview.ts | 2 +- .../models/embedding-response-metadata.ts | 2 +- .../api/generated/models/embedding-usage.ts | 2 +- .../api/generated/models/embedding-warning.ts | 2 +- .../models/generation-response-metadata.ts | 2 +- .../models/image-generation-capability.ts | 2 +- .../models/image-generation-mappings.ts | 2 +- .../models/image-generation-warning.ts | 2 +- ui/src/api/generated/models/image-usage.ts | 2 +- ui/src/api/generated/models/index.ts | 6 + .../api/generated/models/json-patch-inner.ts | 2 +- .../generated/models/language-capability.ts | 2 +- .../api/generated/models/language-mappings.ts | 2 +- ui/src/api/generated/models/metadata.ts | 2 +- .../generated/models/model-capabilities.ts | 11 +- .../models/model-capability-sources.ts | 16 +- .../generated/models/model-message-part.ts | 2 +- ui/src/api/generated/models/model-message.ts | 2 +- .../generated/models/model-option-provider.ts | 2 +- .../models/model-option-unavailable-detail.ts | 2 +- ui/src/api/generated/models/model-option.ts | 5 +- .../models/model-parameter-definition-info.ts | 5 +- .../models/model-parameter-mappings.ts | 2 +- ui/src/api/generated/models/move-operation.ts | 2 +- .../api/generated/models/normalized-usage.ts | 2 +- ui/src/api/generated/models/output-spec.ts | 2 +- .../models/parameter-mapping-template-info.ts | 8 +- .../provider-model-discovery-response.ts | 2 +- .../generated/models/provider-type-info.ts | 8 +- .../api/generated/models/reasoning-mapping.ts | 2 +- .../api/generated/models/reasoning-options.ts | 2 +- .../models/reasoning-value-mapping.ts | 2 +- .../api/generated/models/remove-operation.ts | 2 +- .../api/generated/models/replace-operation.ts | 2 +- .../api/generated/models/rerank-mappings.ts | 2 +- ui/src/api/generated/models/rerank-preview.ts | 2 +- .../models/rerank-response-metadata.ts | 2 +- ui/src/api/generated/models/rerank-usage.ts | 2 +- ui/src/api/generated/models/rerank-warning.ts | 2 +- ui/src/api/generated/models/reset-request.ts | 2 +- ui/src/api/generated/models/selection.ts | 2 +- .../models/test-completion-stream-request.ts | 2 +- .../generated/models/test-decision-request.ts | 53 +++++ .../models/test-embedding-request.ts | 2 +- .../models/test-embedding-response.ts | 2 +- .../generated/models/test-generated-file.ts | 2 +- .../models/test-image-generation-request.ts | 2 +- .../models/test-image-generation-response.ts | 2 +- .../generated/models/test-media-content.ts | 2 +- .../models/test-object-stream-request.ts | 2 +- ui/src/api/generated/models/test-operation.ts | 2 +- .../api/generated/models/test-rag-options.ts | 2 +- .../api/generated/models/test-rag-request.ts | 2 +- .../api/generated/models/test-rag-source.ts | 2 +- .../generated/models/test-rerank-request.ts | 2 +- .../generated/models/test-rerank-response.ts | 2 +- .../models/test-ui-message-chat-request.ts | 2 +- .../api/generated/models/test-ui-message.ts | 2 +- ui/src/api/generated/models/tool-choice.ts | 2 +- .../api/generated/models/uimessage-chunk.ts | 2 +- .../generated/models/usage-breakdown-item.ts | 2 +- .../api/generated/models/usage-breakdown.ts | 2 +- .../api/generated/models/usage-call-detail.ts | 2 +- .../api/generated/models/usage-call-item.ts | 2 +- .../api/generated/models/usage-call-page.ts | 2 +- ui/src/api/generated/models/usage-error.ts | 2 +- .../models/usage-execution-record.ts | 5 +- ui/src/api/generated/models/usage-health.ts | 2 +- ui/src/api/generated/models/usage-summary.ts | 2 +- .../models/usage-trend-dimension-value.ts | 2 +- .../api/generated/models/usage-trend-point.ts | 2 +- .../workbench/use-decision-test.test.ts | 94 ++++++++ .../workbench/use-decision-test.ts | 113 +++++++++ .../workbench/use-workbench-models.ts | 11 +- ui/src/formkit/ai-model-selector.ts | 2 + ui/src/types/index.ts | 1 + ui/src/utils/capabilities.ts | 22 +- ui/src/utils/decision-workbench.ts | 77 ++++++ ui/src/utils/usage.ts | 10 +- ui/src/views/DefaultModelSlotsView.vue | 3 + ui/src/views/ModelTestWorkbenchView.vue | 42 +++- .../workbench/DecisionQuestionEditor.vue | 74 ++++++ .../workbench/DecisionResults.test.ts | 31 +++ .../components/workbench/DecisionResults.vue | 50 ++++ .../workbench/DecisionTestPanel.vue | 79 +++++++ .../components/workbench/WorkbenchToolbar.vue | 1 + 187 files changed, 3422 insertions(+), 161 deletions(-) create mode 100644 api/src/main/java/run/halo/aifoundation/capability/DecisionCapability.java create mode 100644 api/src/main/java/run/halo/aifoundation/decision/DecisionAnswer.java create mode 100644 api/src/main/java/run/halo/aifoundation/decision/DecisionModel.java create mode 100644 api/src/main/java/run/halo/aifoundation/decision/DecisionQuestion.java create mode 100644 api/src/main/java/run/halo/aifoundation/decision/DecisionQuestionType.java create mode 100644 api/src/main/java/run/halo/aifoundation/decision/DecisionRequest.java create mode 100644 api/src/main/java/run/halo/aifoundation/decision/DecisionResponse.java create mode 100644 api/src/main/java/run/halo/aifoundation/decision/DecisionUsage.java create mode 100644 api/src/main/java/run/halo/aifoundation/exception/DecisionCancelledException.java create mode 100644 api/src/main/java/run/halo/aifoundation/exception/DecisionTimeoutException.java create mode 100644 app/src/main/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpoint.java create mode 100644 app/src/main/java/run/halo/aifoundation/provider/support/ProviderDecisionClient.java create mode 100644 app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClient.java create mode 100644 app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java create mode 100644 app/src/main/java/run/halo/aifoundation/service/audit/AuditedDecisionModel.java create mode 100644 app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelFactory.java create mode 100644 app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java create mode 100644 app/src/main/java/run/halo/aifoundation/service/decision/DecisionValidation.java create mode 100644 app/src/test/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpointTest.java create mode 100644 app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java create mode 100644 app/src/test/java/run/halo/aifoundation/service/audit/AuditedDecisionModelTest.java create mode 100644 app/src/test/java/run/halo/aifoundation/service/decision/DecisionModelTest.java create mode 100644 dev/en/sdk-core/decision-models.md create mode 100644 dev/zh-CN/sdk-core/decision-models.md create mode 100644 ui/packages/sdk/src/decision.test.ts create mode 100644 ui/packages/sdk/src/decision.ts create mode 100644 ui/src/api/generated/models/decision-answer.ts create mode 100644 ui/src/api/generated/models/decision-capability.ts create mode 100644 ui/src/api/generated/models/decision-question.ts create mode 100644 ui/src/api/generated/models/decision-response.ts create mode 100644 ui/src/api/generated/models/decision-usage.ts create mode 100644 ui/src/api/generated/models/test-decision-request.ts create mode 100644 ui/src/composables/workbench/use-decision-test.test.ts create mode 100644 ui/src/composables/workbench/use-decision-test.ts create mode 100644 ui/src/utils/decision-workbench.ts create mode 100644 ui/src/views/components/workbench/DecisionQuestionEditor.vue create mode 100644 ui/src/views/components/workbench/DecisionResults.test.ts create mode 100644 ui/src/views/components/workbench/DecisionResults.vue create mode 100644 ui/src/views/components/workbench/DecisionTestPanel.vue diff --git a/README.md b/README.md index d4728a5d..3e1552f3 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Halo 官方 AI 能力平台,统一接入主流大模型,为插件生态提 ## 功能特性 - **多提供商支持**:内置 OpenAI、OpenRouter、DeepSeek、月之暗面 Kimi、硅基流动、阿里云百炼、豆包、文心一言、智谱开放平台、Ollama、OpenAI 兼容、AIHubMix、Gitee 模力方舟、MiniMax、腾讯云 TokenHub 和 Xiaomi MiMo +- **决策模型**:接入 TypeSafe / Jev,统一提供是非概率、单选和评分,以及决策测试工作台 - **统一模型管理**:在 Halo 控制台管理 Provider、API Key Secret、语言 / Embedding / Rerank / 图像生成模型、Adapter 与模型能力 - **模型自动发现**:从 Provider 拉取模型,并保留远程声明、内建能力和管理员覆盖的来源信息 - **统一参数映射**:按 Provider 或模型配置采样、重试、推理、Embedding、Rerank 和图像参数到供应方字段的映射 diff --git a/README_EN.md b/README_EN.md index e2c2e192..88ff5122 100644 --- a/README_EN.md +++ b/README_EN.md @@ -10,6 +10,7 @@ ecosystem. ## Features +- **Decision models**: TypeSafe / Jev integration with typed probability, choice, score results and a decision workbench. - **Multiple AI providers**: Built-in support for OpenAI, OpenRouter, DeepSeek, Moonshot Kimi, SiliconFlow, Alibaba Cloud Model Studio, Doubao, ERNIE, Zhipu AI, Ollama, OpenAI-compatible providers, AIHubMix, Gitee AI, MiniMax, Tencent Cloud TokenHub, and Xiaomi MiMo diff --git a/api-docs/openapi/v3_0/aiFoundationApis.json b/api-docs/openapi/v3_0/aiFoundationApis.json index ae7f3010..af9e1140 100644 --- a/api-docs/openapi/v3_0/aiFoundationApis.json +++ b/api-docs/openapi/v3_0/aiFoundationApis.json @@ -2,7 +2,7 @@ "openapi" : "3.0.1", "info" : { "title" : "Halo", - "version" : "2.26.0" + "version" : "2.26.1" }, "servers" : [ { "url" : "/", @@ -799,6 +799,44 @@ "tags" : [ "console.api.aifoundation.halo.run/v1alpha1/Model" ] } }, + "/apis/console.api.aifoundation.halo.run/v1alpha1/models/{name}/test-decision" : { + "post" : { + "description" : "Evaluate typed questions against a shared text or JSON state.", + "operationId" : "TestModelDecision", + "parameters" : [ { + "description" : "Model name (AiModel.metadata.name)", + "in" : "path", + "name" : "name", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/TestDecisionRequest" + } + } + }, + "required" : true + }, + "responses" : { + "default" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/DecisionResponse" + } + } + }, + "description" : "default response" + } + }, + "tags" : [ "console.api.aifoundation.halo.run/v1alpha1/Model" ] + } + }, "/apis/console.api.aifoundation.halo.run/v1alpha1/models/{name}/test-embedding" : { "post" : { "description" : "Test embedding generation with embedding settings and diagnostics.", @@ -1698,7 +1736,7 @@ "properties" : { "adapterType" : { "type" : "string", - "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image" ] + "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one" ] }, "defaultParameterMappingOverrides" : { "type" : "object", @@ -1715,7 +1753,7 @@ }, "modelType" : { "type" : "string", - "enum" : [ "language", "embedding", "rerank", "image-generation" ] + "enum" : [ "language", "embedding", "rerank", "image-generation", "decision" ] }, "recommended" : { "type" : "boolean" @@ -1823,7 +1861,7 @@ "adapterType" : { "type" : "string", "description" : "Internal invocation adapter", - "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image" ] + "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one" ] }, "capabilities" : { "$ref" : "#/components/schemas/ModelCapabilities" @@ -1865,7 +1903,7 @@ "modelType" : { "type" : "string", "description" : "Primary model purpose", - "enum" : [ "language", "embedding", "rerank", "image-generation" ] + "enum" : [ "language", "embedding", "rerank", "image-generation", "decision" ] }, "nativeOptions" : { "type" : "object", @@ -2106,9 +2144,133 @@ } } }, + "DecisionAnswer" : { + "type" : "object", + "properties" : { + "choice" : { + "type" : "string" + }, + "confidence" : { + "type" : "number", + "format" : "double" + }, + "legend" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "probabilities" : { + "type" : "object", + "additionalProperties" : { + "type" : "number", + "format" : "double" + } + }, + "probability" : { + "type" : "number", + "format" : "double" + }, + "score" : { + "type" : "number", + "format" : "double" + }, + "type" : { + "type" : "string", + "enum" : [ "YES_NO", "CHOICE", "SCORE" ] + } + } + }, + "DecisionCapability" : { + "type" : "object", + "properties" : { + "maxChoiceOptions" : { + "type" : "integer", + "format" : "int32" + }, + "maxScoreLevels" : { + "type" : "integer", + "format" : "int32" + }, + "questionTypes" : { + "type" : "array", + "items" : { + "type" : "string", + "enum" : [ "YES_NO", "CHOICE", "SCORE" ] + } + }, + "structuredState" : { + "type" : "boolean" + } + } + }, + "DecisionQuestion" : { + "type" : "object", + "properties" : { + "instructions" : { + "type" : "string" + }, + "levels" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "noCriteria" : { + "type" : "string" + }, + "options" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "type" : { + "type" : "string", + "enum" : [ "YES_NO", "CHOICE", "SCORE" ] + }, + "yesCriteria" : { + "type" : "string" + } + }, + "description" : "Independent questions keyed by caller-selected identifiers" + }, + "DecisionResponse" : { + "type" : "object", + "properties" : { + "answers" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/components/schemas/DecisionAnswer" + } + }, + "model" : { + "type" : "string" + }, + "usage" : { + "$ref" : "#/components/schemas/DecisionUsage" + } + } + }, + "DecisionUsage" : { + "type" : "object", + "properties" : { + "inputTokens" : { + "type" : "integer", + "format" : "int32" + }, + "outputTokens" : { + "type" : "integer", + "format" : "int32" + } + } + }, "DefaultModelSlots" : { "type" : "object", "properties" : { + "decisionModelName" : { + "type" : "string" + }, "embeddingModelName" : { "type" : "string" }, @@ -2140,7 +2302,7 @@ "adapterType" : { "type" : "string", "nullable" : true, - "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image" ] + "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one" ] }, "capabilities" : { "$ref" : "#/components/schemas/ModelCapabilities" @@ -2167,7 +2329,7 @@ }, "modelType" : { "type" : "string", - "enum" : [ "language", "embedding", "rerank", "image-generation" ] + "enum" : [ "language", "embedding", "rerank", "image-generation", "decision" ] }, "name" : { "type" : "string" @@ -2564,6 +2726,9 @@ "ModelCapabilities" : { "type" : "object", "properties" : { + "decision" : { + "$ref" : "#/components/schemas/DecisionCapability" + }, "imageGeneration" : { "$ref" : "#/components/schemas/ImageGenerationCapability" }, @@ -2578,6 +2743,10 @@ "ModelCapabilitySources" : { "type" : "object", "properties" : { + "decision" : { + "type" : "string", + "enum" : [ "remote", "built-in", "manual", "unknown" ] + }, "imageGeneration" : { "type" : "string", "enum" : [ "remote", "built-in", "manual", "unknown" ] @@ -2694,7 +2863,7 @@ "modelType" : { "type" : "string", "description" : "Primary model purpose", - "enum" : [ "language", "embedding", "rerank", "image-generation" ] + "enum" : [ "language", "embedding", "rerank", "image-generation", "decision" ] }, "name" : { "type" : "string", @@ -2800,7 +2969,7 @@ }, "modelType" : { "type" : "string", - "enum" : [ "language", "embedding", "rerank", "image-generation" ] + "enum" : [ "language", "embedding", "rerank", "image-generation", "decision" ] }, "parameter" : { "type" : "string" @@ -2927,7 +3096,7 @@ "type" : "array", "items" : { "type" : "string", - "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image" ] + "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one" ] } }, "configurationType" : { @@ -2950,7 +3119,7 @@ }, "modelType" : { "type" : "string", - "enum" : [ "language", "embedding", "rerank", "image-generation" ] + "enum" : [ "language", "embedding", "rerank", "image-generation", "decision" ] }, "parameter" : { "type" : "string" @@ -3051,7 +3220,7 @@ "type" : "array", "items" : { "type" : "string", - "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image" ] + "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one" ] } }, "supportedFeatures" : { @@ -3065,7 +3234,7 @@ "type" : "array", "items" : { "type" : "string", - "enum" : [ "language", "embedding", "rerank", "image-generation" ] + "enum" : [ "language", "embedding", "rerank", "image-generation", "decision" ] } }, "websiteUrl" : { @@ -3362,6 +3531,28 @@ } } }, + "TestDecisionRequest" : { + "required" : [ "questions", "state" ], + "type" : "object", + "properties" : { + "questions" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/components/schemas/DecisionQuestion" + }, + "description" : "Independent questions keyed by caller-selected identifiers" + }, + "state" : { + "type" : "string", + "description" : "Text to evaluate, or serialized JSON when stateFormat is JSON" + }, + "stateFormat" : { + "type" : "string", + "description" : "Input format; defaults to TEXT", + "enum" : [ "TEXT", "JSON" ] + } + } + }, "TestEmbeddingRequest" : { "type" : "object", "properties" : { @@ -4265,7 +4456,7 @@ }, "unitKind" : { "type" : "string", - "enum" : [ "GENERATION_STEP", "EMBEDDING_BATCH", "RERANK", "IMAGE_BATCH" ] + "enum" : [ "GENERATION_STEP", "EMBEDDING_BATCH", "RERANK", "IMAGE_BATCH", "DECISION" ] }, "usage" : { "$ref" : "#/components/schemas/NormalizedUsage" diff --git a/api/src/main/java/run/halo/aifoundation/AiModelService.java b/api/src/main/java/run/halo/aifoundation/AiModelService.java index 4529402c..989b3d7d 100644 --- a/api/src/main/java/run/halo/aifoundation/AiModelService.java +++ b/api/src/main/java/run/halo/aifoundation/AiModelService.java @@ -3,6 +3,7 @@ import org.pf4j.ExtensionPoint; import reactor.core.publisher.Mono; import run.halo.aifoundation.chat.LanguageModel; +import run.halo.aifoundation.decision.DecisionModel; import run.halo.aifoundation.embedding.EmbeddingModel; import run.halo.aifoundation.image.ImageGenerationModel; import run.halo.aifoundation.rerank.RerankingModel; @@ -17,6 +18,19 @@ */ public interface AiModelService extends ExtensionPoint { + /** Resolves the configured default decision model. */ + default Mono decisionModel() { + return decisionModel(null); + } + + /** + * Resolves a decision model by {@code AiModel.metadata.name}; null/blank selects the default. + * The default method preserves compatibility with implementations compiled before this API. + */ + default Mono decisionModel(String modelName) { + return Mono.error(new UnsupportedOperationException("Decision models are not supported")); + } + /** * Resolves the configured default language model. * diff --git a/api/src/main/java/run/halo/aifoundation/capability/CapabilityDomain.java b/api/src/main/java/run/halo/aifoundation/capability/CapabilityDomain.java index 9f4b46f7..306a913c 100644 --- a/api/src/main/java/run/halo/aifoundation/capability/CapabilityDomain.java +++ b/api/src/main/java/run/halo/aifoundation/capability/CapabilityDomain.java @@ -21,7 +21,10 @@ public enum CapabilityDomain { /** * Image generation capabilities, including generation modes and output controls. */ - IMAGE_GENERATION("imageGeneration"); + IMAGE_GENERATION("imageGeneration"), + + /** Decision question types, structured input support, and option limits. */ + DECISION("decision"); /** * Stable serialized value used in resource specs, API payloads, and selector filters. diff --git a/api/src/main/java/run/halo/aifoundation/capability/DecisionCapability.java b/api/src/main/java/run/halo/aifoundation/capability/DecisionCapability.java new file mode 100644 index 00000000..04758ffb --- /dev/null +++ b/api/src/main/java/run/halo/aifoundation/capability/DecisionCapability.java @@ -0,0 +1,20 @@ +package run.halo.aifoundation.capability; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import run.halo.aifoundation.decision.DecisionQuestionType; + +/** Capabilities of a decision adapter. Null limits are unknown, not unlimited. */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DecisionCapability { + private List questionTypes; + private Boolean structuredState; + private Integer maxChoiceOptions; + private Integer maxScoreLevels; +} diff --git a/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilities.java b/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilities.java index 25d4e451..36d9a087 100644 --- a/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilities.java +++ b/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilities.java @@ -19,6 +19,15 @@ @AllArgsConstructor public class ModelCapabilities { + /** Decision capabilities supplied by the active provider adapter. */ + private DecisionCapability decision; + + /** Retains the constructor published before decision models were introduced. */ + public ModelCapabilities(LanguageCapability language, + ImageGenerationCapability imageGeneration, ModelCapabilitySources sources) { + this(null, language, imageGeneration, sources); + } + /** * Fine-grained language model capabilities, or {@code null} when unknown or not applicable. */ diff --git a/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilityRequirement.java b/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilityRequirement.java index 2ed9d2e4..799b9fdf 100644 --- a/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilityRequirement.java +++ b/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilityRequirement.java @@ -20,6 +20,14 @@ @AllArgsConstructor public class ModelCapabilityRequirement { + private DecisionCapability decision; + + /** Retains the constructor published before decision requirements were introduced. */ + public ModelCapabilityRequirement(LanguageCapability language, + ImageGenerationCapability imageGeneration) { + this(null, language, imageGeneration); + } + /** * Required language capabilities. All non-null fields must be satisfied. */ diff --git a/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilitySources.java b/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilitySources.java index 0a7230db..01dccbe4 100644 --- a/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilitySources.java +++ b/api/src/main/java/run/halo/aifoundation/capability/ModelCapabilitySources.java @@ -18,6 +18,13 @@ @AllArgsConstructor public class ModelCapabilitySources { + private CapabilitySource decision; + + /** Retains the constructor published before decision models were introduced. */ + public ModelCapabilitySources(CapabilitySource language, CapabilitySource imageGeneration) { + this(null, language, imageGeneration); + } + /** * Source for {@link ModelCapabilities#getLanguage()}. */ @@ -39,6 +46,7 @@ public CapabilitySource source(CapabilityDomain domain) { return null; } return switch (domain) { + case DECISION -> decision; case LANGUAGE -> language; case IMAGE_GENERATION -> imageGeneration; }; @@ -51,6 +59,7 @@ public CapabilitySource source(CapabilityDomain domain) { */ public static ModelCapabilitySources unknown() { return ModelCapabilitySources.builder() + .decision(CapabilitySource.UNKNOWN) .language(CapabilitySource.UNKNOWN) .imageGeneration(CapabilitySource.UNKNOWN) .build(); diff --git a/api/src/main/java/run/halo/aifoundation/decision/DecisionAnswer.java b/api/src/main/java/run/halo/aifoundation/decision/DecisionAnswer.java new file mode 100644 index 00000000..b740dc87 --- /dev/null +++ b/api/src/main/java/run/halo/aifoundation/decision/DecisionAnswer.java @@ -0,0 +1,28 @@ +package run.halo.aifoundation.decision; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** A typed decision. Probability and confidence are distinct quantities. */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DecisionAnswer { + private DecisionQuestionType type; + /** YES_NO probability of yes, in [0, 1]; not a thresholded boolean. */ + private Double probability; + /** Selected CHOICE option identifier. */ + private String choice; + /** SCORE's probability-weighted level index, which may be fractional. */ + private Double score; + /** Distribution over option identifiers or zero-based score level indices. */ + private Map probabilities; + /** Distribution-derived certainty for CHOICE/SCORE, absent for YES_NO. */ + private Double confidence; + /** SCORE level indices and descriptions. */ + private Map legend; +} diff --git a/api/src/main/java/run/halo/aifoundation/decision/DecisionModel.java b/api/src/main/java/run/halo/aifoundation/decision/DecisionModel.java new file mode 100644 index 00000000..3a55cd14 --- /dev/null +++ b/api/src/main/java/run/halo/aifoundation/decision/DecisionModel.java @@ -0,0 +1,14 @@ +package run.halo.aifoundation.decision; + +import reactor.core.publisher.Mono; +import run.halo.aifoundation.capability.DecisionCapability; + +/** Provider-neutral inference for closed decisions. Callers own thresholds and actions. */ +public interface DecisionModel { + Mono decide(DecisionRequest request); + + /** Read-only capability snapshot, or null when unavailable. */ + default DecisionCapability capabilities() { + return null; + } +} diff --git a/api/src/main/java/run/halo/aifoundation/decision/DecisionQuestion.java b/api/src/main/java/run/halo/aifoundation/decision/DecisionQuestion.java new file mode 100644 index 00000000..7e177bf2 --- /dev/null +++ b/api/src/main/java/run/halo/aifoundation/decision/DecisionQuestion.java @@ -0,0 +1,40 @@ +package run.halo.aifoundation.decision; + +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** A closed question evaluated against the request's shared state. */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DecisionQuestion { + private DecisionQuestionType type; + private String instructions; + /** CHOICE option identifiers and descriptions. */ + private Map options; + /** SCORE descriptions in ascending order; the first level has index zero. */ + private List levels; + /** Optional YES_NO criteria describing a positive answer. */ + private String yesCriteria; + /** Optional YES_NO criteria describing a negative answer. */ + private String noCriteria; + + public static DecisionQuestion yesNo(String instructions) { + return builder().type(DecisionQuestionType.YES_NO).instructions(instructions).build(); + } + + public static DecisionQuestion choice(String instructions, Map options) { + return builder().type(DecisionQuestionType.CHOICE).instructions(instructions) + .options(options).build(); + } + + public static DecisionQuestion score(String instructions, List levels) { + return builder().type(DecisionQuestionType.SCORE).instructions(instructions) + .levels(levels).build(); + } +} diff --git a/api/src/main/java/run/halo/aifoundation/decision/DecisionQuestionType.java b/api/src/main/java/run/halo/aifoundation/decision/DecisionQuestionType.java new file mode 100644 index 00000000..de4a04cc --- /dev/null +++ b/api/src/main/java/run/halo/aifoundation/decision/DecisionQuestionType.java @@ -0,0 +1,8 @@ +package run.halo.aifoundation.decision; + +/** Provider-neutral kinds of closed questions. */ +public enum DecisionQuestionType { + YES_NO, + CHOICE, + SCORE +} diff --git a/api/src/main/java/run/halo/aifoundation/decision/DecisionRequest.java b/api/src/main/java/run/halo/aifoundation/decision/DecisionRequest.java new file mode 100644 index 00000000..4bfd729e --- /dev/null +++ b/api/src/main/java/run/halo/aifoundation/decision/DecisionRequest.java @@ -0,0 +1,35 @@ +package run.halo.aifoundation.decision; + +import java.beans.Transient; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import run.halo.aifoundation.chat.GenerationTimeouts; +import run.halo.aifoundation.control.CancellationToken; + +/** One shared textual or JSON state and independently evaluated named questions. */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DecisionRequest { + /** Text, a JSON object, or a JSON array; media assets are not supported. */ + private Object state; + private Map questions; + /** Local audit metadata, never sent to the provider. */ + private Map metadata; + private transient CancellationToken cancellationToken; + private transient GenerationTimeouts timeouts; + + @Transient + public CancellationToken getCancellationToken() { + return cancellationToken; + } + + @Transient + public GenerationTimeouts getTimeouts() { + return timeouts; + } +} diff --git a/api/src/main/java/run/halo/aifoundation/decision/DecisionResponse.java b/api/src/main/java/run/halo/aifoundation/decision/DecisionResponse.java new file mode 100644 index 00000000..0da22853 --- /dev/null +++ b/api/src/main/java/run/halo/aifoundation/decision/DecisionResponse.java @@ -0,0 +1,18 @@ +package run.halo.aifoundation.decision; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** Answers keyed by the original question identifiers, with provider-reported model and usage. */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DecisionResponse { + private String model; + private Map answers; + private DecisionUsage usage; +} diff --git a/api/src/main/java/run/halo/aifoundation/decision/DecisionUsage.java b/api/src/main/java/run/halo/aifoundation/decision/DecisionUsage.java new file mode 100644 index 00000000..7a7a37b9 --- /dev/null +++ b/api/src/main/java/run/halo/aifoundation/decision/DecisionUsage.java @@ -0,0 +1,16 @@ +package run.halo.aifoundation.decision; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** Reported token counts; absent counts stay unknown, rather than becoming zero. */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DecisionUsage { + private Integer inputTokens; + private Integer outputTokens; +} diff --git a/api/src/main/java/run/halo/aifoundation/exception/DecisionCancelledException.java b/api/src/main/java/run/halo/aifoundation/exception/DecisionCancelledException.java new file mode 100644 index 00000000..19093222 --- /dev/null +++ b/api/src/main/java/run/halo/aifoundation/exception/DecisionCancelledException.java @@ -0,0 +1,8 @@ +package run.halo.aifoundation.exception; + +/** Raised when a decision request is cancelled through its cancellation token. */ +public class DecisionCancelledException extends AiFoundationException { + public DecisionCancelledException() { + super("Decision inference was cancelled"); + } +} diff --git a/api/src/main/java/run/halo/aifoundation/exception/DecisionTimeoutException.java b/api/src/main/java/run/halo/aifoundation/exception/DecisionTimeoutException.java new file mode 100644 index 00000000..213c7e4f --- /dev/null +++ b/api/src/main/java/run/halo/aifoundation/exception/DecisionTimeoutException.java @@ -0,0 +1,17 @@ +package run.halo.aifoundation.exception; + +import java.time.Duration; + +/** Raised when decision inference exceeds its total timeout. */ +public class DecisionTimeoutException extends AiFoundationException { + private final Duration timeout; + + public DecisionTimeoutException(Duration timeout, Throwable cause) { + super("AI decision inference timed out after " + timeout, cause); + this.timeout = timeout; + } + + public Duration getTimeout() { + return timeout; + } +} diff --git a/app/src/main/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpoint.java b/app/src/main/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpoint.java new file mode 100644 index 00000000..9c39e084 --- /dev/null +++ b/app/src/main/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpoint.java @@ -0,0 +1,99 @@ +package run.halo.aifoundation.endpoint; + +import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; +import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; +import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder; +import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route; + +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ResponseStatusException; +import reactor.core.publisher.Mono; +import run.halo.aifoundation.AiModelService; +import run.halo.aifoundation.decision.DecisionQuestion; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.decision.DecisionResponse; +import run.halo.aifoundation.service.decision.DecisionValidation; +import run.halo.app.core.extension.endpoint.CustomEndpoint; +import run.halo.app.extension.GroupVersion; +import tools.jackson.databind.json.JsonMapper; + +@Component +@RequiredArgsConstructor +public class DecisionConsoleEndpoint implements CustomEndpoint { + private static final JsonMapper JSON_MAPPER = JsonMapper.builder().build(); + private final AiModelService modelService; + + @Override + public RouterFunction endpoint() { + return route().POST("models/{name}/test-decision", this::testDecision, + builder -> builder.operationId("TestModelDecision") + .description("Evaluate typed questions against a shared text or JSON state.") + .tag("console.api.aifoundation.halo.run/v1alpha1/Model") + .parameter(parameterBuilder().name("name").in(ParameterIn.PATH) + .description("Model name (AiModel.metadata.name)") + .implementation(String.class).required(true)) + .requestBody(requestBodyBuilder().required(true) + .implementation(TestDecisionRequest.class)) + .response(responseBuilder().implementation(DecisionResponse.class))) + .build(); + } + + @Override + public GroupVersion groupVersion() { + return GroupVersion.parseAPIVersion("console.api.aifoundation.halo.run/v1alpha1"); + } + + private Mono testDecision(ServerRequest request) { + return request.bodyToMono(TestDecisionRequest.class) + .switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Decision request is required"))) + .flatMap(body -> { + var input = DecisionRequest.builder().state(parseState(body)) + .questions(body.getQuestions()).metadata(Map.of("source", "console-test")) + .build(); + return Mono.fromRunnable(() -> DecisionValidation.request(input)) + .then(Mono.defer(() -> modelService.decisionModel(request.pathVariable("name")))) + .flatMap(model -> model.decide(input)) + .onErrorMap(IllegalArgumentException.class, + error -> new ResponseStatusException(HttpStatus.BAD_REQUEST, + error.getMessage(), error)); + }) + .flatMap(response -> ServerResponse.ok().bodyValue(response)); + } + + @Data + public static class TestDecisionRequest { + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, + description = "Text to evaluate, or serialized JSON when stateFormat is JSON") + private String state; + @Schema(description = "Input format; defaults to TEXT") + private StateFormat stateFormat = StateFormat.TEXT; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, + description = "Independent questions keyed by caller-selected identifiers") + private Map questions; + } + + public enum StateFormat { + TEXT, JSON + } + + private static Object parseState(TestDecisionRequest request) { + if (request.getStateFormat() != StateFormat.JSON) { + return request.getState(); + } + try { + return JSON_MAPPER.readValue(request.getState(), Object.class); + } catch (RuntimeException error) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "State must be valid JSON", error); + } + } +} diff --git a/app/src/main/java/run/halo/aifoundation/endpoint/DefaultModelSlotConsoleEndpoint.java b/app/src/main/java/run/halo/aifoundation/endpoint/DefaultModelSlotConsoleEndpoint.java index 17069913..b0dfe0b9 100644 --- a/app/src/main/java/run/halo/aifoundation/endpoint/DefaultModelSlotConsoleEndpoint.java +++ b/app/src/main/java/run/halo/aifoundation/endpoint/DefaultModelSlotConsoleEndpoint.java @@ -72,6 +72,7 @@ private Mono updateDefaultModelSlots(ServerRequest request) { private Mono validateSlots(DefaultModelSlots slots) { return Flux.concat(List.of( + validateSlot("decisionModelName", slots.getDecisionModelName(), ModelType.DECISION), validateSlot("languageModelName", slots.getLanguageModelName(), ModelType.LANGUAGE), validateSlot("embeddingModelName", slots.getEmbeddingModelName(), ModelType.EMBEDDING), validateSlot("rerankModelName", slots.getRerankModelName(), ModelType.RERANK), diff --git a/app/src/main/java/run/halo/aifoundation/endpoint/ModelOptionQuery.java b/app/src/main/java/run/halo/aifoundation/endpoint/ModelOptionQuery.java index 25306420..86aecf7e 100644 --- a/app/src/main/java/run/halo/aifoundation/endpoint/ModelOptionQuery.java +++ b/app/src/main/java/run/halo/aifoundation/endpoint/ModelOptionQuery.java @@ -8,10 +8,12 @@ import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.server.ResponseStatusException; +import run.halo.aifoundation.capability.DecisionCapability; import run.halo.aifoundation.capability.ImageGenerationCapability; import run.halo.aifoundation.capability.InputSource; import run.halo.aifoundation.capability.LanguageCapability; import run.halo.aifoundation.capability.ModelCapabilityRequirement; +import run.halo.aifoundation.decision.DecisionQuestionType; import run.halo.aifoundation.provider.support.ModelFeature; import run.halo.aifoundation.provider.support.ModelType; import run.halo.app.extension.ListOptions; @@ -185,8 +187,9 @@ private static ModelCapabilityRequirement parseRequiredCapabilities(String value } catch (JacksonException e) { throw badRequest("requiredCapabilities must be valid JSON"); } - rejectUnknownKeys(root, "requiredCapabilities", List.of("language", "imageGeneration")); + rejectUnknownKeys(root, "requiredCapabilities", List.of("language", "imageGeneration", "decision")); return ModelCapabilityRequirement.builder() + .decision(parseDecisionCapability(asMap(root.get("decision"), "decision"))) .language(parseLanguageCapability(asMap(root.get("language"), "language"))) .imageGeneration(parseImageGenerationCapability( asMap(root.get("imageGeneration"), "imageGeneration"))) @@ -210,6 +213,29 @@ private static LanguageCapability parseLanguageCapability(Map value) { .build(); } + private static DecisionCapability parseDecisionCapability(Map value) { + if (value == null) { + return null; + } + rejectUnknownKeys(value, "decision", List.of("questionTypes", "structuredState", + "maxChoiceOptions", "maxScoreLevels")); + var types = stringList(value.get("questionTypes"), "decision.questionTypes"); + var builder = DecisionCapability.builder() + .structuredState(booleanValue(value.get("structuredState"), "decision.structuredState")) + .maxChoiceOptions(integerValue(value.get("maxChoiceOptions"), "decision.maxChoiceOptions")) + .maxScoreLevels(integerValue(value.get("maxScoreLevels"), "decision.maxScoreLevels")); + if (types != null) { + builder.questionTypes(types.stream().map(type -> { + try { + return DecisionQuestionType.valueOf(type); + } catch (IllegalArgumentException error) { + throw badRequest("Unsupported decision question type: " + type); + } + }).toList()); + } + return builder.build(); + } + private static ImageGenerationCapability parseImageGenerationCapability(Map value) { if (value == null) { return null; diff --git a/app/src/main/java/run/halo/aifoundation/provider/AiProviderType.java b/app/src/main/java/run/halo/aifoundation/provider/AiProviderType.java index f83efe49..d5ad86c7 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/AiProviderType.java +++ b/app/src/main/java/run/halo/aifoundation/provider/AiProviderType.java @@ -7,6 +7,7 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.lang.Nullable; import reactor.core.publisher.Mono; +import run.halo.aifoundation.capability.DecisionCapability; import run.halo.aifoundation.extension.AiProvider; import run.halo.aifoundation.provider.mapping.DefaultParameterMapping; import run.halo.aifoundation.provider.mapping.ModelParameter; @@ -18,6 +19,7 @@ import run.halo.aifoundation.provider.support.ModelFeature; import run.halo.aifoundation.provider.support.ModelType; import run.halo.aifoundation.provider.support.NativeModelOptionsValidator; +import run.halo.aifoundation.provider.support.ProviderDecisionClient; import run.halo.aifoundation.provider.support.ProviderImageGenerationClient; import run.halo.aifoundation.provider.support.ProviderModelRef; import run.halo.aifoundation.provider.support.ProviderModelResolver; @@ -124,6 +126,15 @@ default List getSupportedFeatures(AdapterType adapterType) { // ── Behavior ────────────────────────────────────────────── + default DecisionCapability decisionCapabilities() { + return null; + } + + default ProviderDecisionClient buildDecisionClient( + AiProvider provider, String apiKey, ProviderModelRef model) { + return null; + } + ChatModel buildChatModel(AiProvider provider, String apiKey, String modelId); default ChatModel buildChatModel(AiProvider provider, String apiKey, ProviderModelRef model) { diff --git a/app/src/main/java/run/halo/aifoundation/provider/support/AdapterProtocol.java b/app/src/main/java/run/halo/aifoundation/provider/support/AdapterProtocol.java index a3cf149e..22305630 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/support/AdapterProtocol.java +++ b/app/src/main/java/run/halo/aifoundation/provider/support/AdapterProtocol.java @@ -43,7 +43,8 @@ public enum AdapterProtocol { OLLAMA_CHAT( "Ollama Chat API", "使用 Ollama 原生 /api/chat 接口。" - ); + ), + SYSTEM_ONE("System One", "使用状态和类型化问题调用决策接口。"); private final String displayName; private final String description; diff --git a/app/src/main/java/run/halo/aifoundation/provider/support/AdapterType.java b/app/src/main/java/run/halo/aifoundation/provider/support/AdapterType.java index d3eb59b4..33885424 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/support/AdapterType.java +++ b/app/src/main/java/run/halo/aifoundation/provider/support/AdapterType.java @@ -92,7 +92,8 @@ public enum AdapterType { OLLAMA_RESPONSES("ollama-responses", ModelType.LANGUAGE, AdapterProtocol.RESPONSES), OLLAMA_MESSAGES("ollama-messages", ModelType.LANGUAGE, AdapterProtocol.MESSAGES), OLLAMA_EMBEDDING("ollama-embedding", ModelType.EMBEDDING, AdapterProtocol.EMBEDDING), - OLLAMA_IMAGE("ollama-image", ModelType.IMAGE_GENERATION, AdapterProtocol.IMAGE_GENERATION); + OLLAMA_IMAGE("ollama-image", ModelType.IMAGE_GENERATION, AdapterProtocol.IMAGE_GENERATION), + TYPESAFE_SYSTEM_ONE("typesafe-system-one", ModelType.DECISION, AdapterProtocol.SYSTEM_ONE); private final String value; private final ModelType modelType; diff --git a/app/src/main/java/run/halo/aifoundation/provider/support/ModelType.java b/app/src/main/java/run/halo/aifoundation/provider/support/ModelType.java index 6cc0a8c2..6f4a8ed3 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/support/ModelType.java +++ b/app/src/main/java/run/halo/aifoundation/provider/support/ModelType.java @@ -13,7 +13,8 @@ public enum ModelType { LANGUAGE("language"), EMBEDDING("embedding"), RERANK("rerank"), - IMAGE_GENERATION("image-generation"); + IMAGE_GENERATION("image-generation"), + DECISION("decision"); private final String value; diff --git a/app/src/main/java/run/halo/aifoundation/provider/support/ProviderClientCache.java b/app/src/main/java/run/halo/aifoundation/provider/support/ProviderClientCache.java index c69f4603..f9a47859 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/support/ProviderClientCache.java +++ b/app/src/main/java/run/halo/aifoundation/provider/support/ProviderClientCache.java @@ -14,6 +14,7 @@ @Component public class ProviderClientCache { + private final Map decisionClientCache = new ConcurrentHashMap<>(); private final Map chatModelCache = new ConcurrentHashMap<>(); private final Map embeddingModelCache = new ConcurrentHashMap<>(); private final Map rerankingClientCache = @@ -123,8 +124,18 @@ public ProviderImageGenerationClient getOrCreateImageGenerationClient(AiProvider return prev != null ? prev : client; } + public ProviderDecisionClient getOrCreateDecisionClient(AiProvider provider, String apiKey, + ProviderModelRef model) { + var type = getProviderType(provider.getSpec().getProviderType()); + var resolved = ProviderModelResolver.resolve(type, model); + var key = cacheKey(provider.getMetadata().getName(), resolved); + return decisionClientCache.computeIfAbsent(key, + ignored -> type.buildDecisionClient(provider, apiKey, resolved)); + } + public void invalidate(String providerName) { var prefix = providerName + "/"; + decisionClientCache.keySet().removeIf(key -> key.startsWith(prefix)); chatModelCache.keySet().removeIf(key -> key.startsWith(prefix)); embeddingModelCache.keySet().removeIf(key -> key.startsWith(prefix)); rerankingClientCache.keySet().removeIf(key -> key.startsWith(prefix)); @@ -133,6 +144,7 @@ public void invalidate(String providerName) { } public void invalidateAll() { + decisionClientCache.clear(); chatModelCache.clear(); embeddingModelCache.clear(); rerankingClientCache.clear(); diff --git a/app/src/main/java/run/halo/aifoundation/provider/support/ProviderDecisionClient.java b/app/src/main/java/run/halo/aifoundation/provider/support/ProviderDecisionClient.java new file mode 100644 index 00000000..7aa25e7e --- /dev/null +++ b/app/src/main/java/run/halo/aifoundation/provider/support/ProviderDecisionClient.java @@ -0,0 +1,11 @@ +package run.halo.aifoundation.provider.support; + +import reactor.core.publisher.Mono; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.decision.DecisionResponse; + +/** Low-level provider adapter for typed decisions. */ +@FunctionalInterface +public interface ProviderDecisionClient { + Mono decide(DecisionRequest request); +} diff --git a/app/src/main/java/run/halo/aifoundation/provider/support/ProviderModelResolver.java b/app/src/main/java/run/halo/aifoundation/provider/support/ProviderModelResolver.java index 79d9f540..193cc392 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/support/ProviderModelResolver.java +++ b/app/src/main/java/run/halo/aifoundation/provider/support/ProviderModelResolver.java @@ -38,6 +38,7 @@ private static boolean isLegacyGenericAdapter(AiProviderType providerType, return false; } return switch (modelType) { + case DECISION -> false; case LANGUAGE -> adapterType == AdapterType.OPENAI_CHAT; case EMBEDDING -> adapterType == AdapterType.OPENAI_EMBEDDING; case IMAGE_GENERATION -> adapterType == AdapterType.OPENAI_IMAGE; diff --git a/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClient.java b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClient.java new file mode 100644 index 00000000..ebd555de --- /dev/null +++ b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClient.java @@ -0,0 +1,161 @@ +package run.halo.aifoundation.provider.typesafe; + +import java.net.URI; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import run.halo.aifoundation.decision.DecisionAnswer; +import run.halo.aifoundation.decision.DecisionQuestion; +import run.halo.aifoundation.decision.DecisionQuestionType; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.decision.DecisionResponse; +import run.halo.aifoundation.decision.DecisionUsage; +import run.halo.aifoundation.provider.support.ProviderDecisionClient; +import run.halo.aifoundation.provider.transport.ProviderDiagnostics; +import run.halo.aifoundation.provider.transport.ProviderHttpResponseSupport; + +/** TypeSafe System One wire protocol; vendor names do not leak into the public SDK. */ +public class TypeSafeDecisionClient implements ProviderDecisionClient { + private static final ParameterizedTypeReference> JSON = + new ParameterizedTypeReference<>() { }; + + private final URI endpoint; + private final String apiKey; + private final String model; + private final WebClient client; + + public TypeSafeDecisionClient(String baseUrl, String apiKey, String model, + WebClient.Builder builder) { + this.endpoint = URI.create(baseUrl.replaceAll("/+$", "") + "/v1/systemone"); + this.apiKey = apiKey; + this.model = model; + this.client = builder.build(); + } + + @Override + public Mono decide(DecisionRequest request) { + return Mono.defer(() -> { + var questions = new LinkedHashMap(); + request.getQuestions().forEach((id, question) -> questions.put(id, encode(question))); + var body = Map.of("model", model, "state", request.getState(), "questions", questions); + var diagnostics = ProviderDiagnostics.create("typesafe", "decision"); + diagnostics.request(endpoint.toString(), body, false); + return client.post().uri(endpoint).headers(headers -> headers.setBearerAuth(apiKey)) + .bodyValue(body).exchangeToMono(response -> { + if (!response.statusCode().is2xxSuccessful()) { + return ProviderHttpResponseSupport.errorMono(response, "typesafe", + "decision", diagnostics); + } + diagnostics.responseStatus(response.statusCode().value()); + return response.bodyToMono(JSON).map(TypeSafeDecisionClient::decode); + }); + }); + } + + private static Map encode(DecisionQuestion question) { + var body = new LinkedHashMap(); + body.put("instructions", question.getInstructions()); + switch (question.getType()) { + case YES_NO -> { + body.put("type", "noul"); + var criteria = new LinkedHashMap(); + if (question.getYesCriteria() != null) { + criteria.put("true", question.getYesCriteria()); + } + if (question.getNoCriteria() != null) { + criteria.put("false", question.getNoCriteria()); + } + if (!criteria.isEmpty()) { + body.put("criteria", criteria); + } + } + case CHOICE -> { + if (question.getOptions().size() > 255) { + throw new IllegalArgumentException("TypeSafe supports at most 255 choice options"); + } + body.put("type", "choice"); + body.put("criteria", question.getOptions()); + } + case SCORE -> { + if (question.getLevels().size() > 10) { + throw new IllegalArgumentException("TypeSafe supports at most 10 score levels"); + } + body.put("type", "score"); + body.put("criteria", question.getLevels()); + } + } + return body; + } + + private static DecisionResponse decode(Map body) { + var answers = new LinkedHashMap(); + object(body.get("answers")).forEach((id, raw) -> answers.put(id, answer(object(raw)))); + DecisionUsage usage = null; + if (body.get("usage") != null) { + var raw = object(body.get("usage")); + usage = new DecisionUsage(tokens(raw.get("input_tokens")), tokens(raw.get("output_tokens"))); + } + return new DecisionResponse(string(body.get("model")), answers, usage); + } + + private static DecisionAnswer answer(Map raw) { + var answer = DecisionAnswer.builder(); + switch (string(raw.get("type"))) { + case "noul" -> answer.type(DecisionQuestionType.YES_NO).probability(number(raw.get("noul"))); + case "choice" -> answer.type(DecisionQuestionType.CHOICE) + .choice(string(raw.get("choice"))) + .probabilities(probabilities(raw.get("probabilities"))) + .confidence(number(raw.get("confidence"))); + case "score" -> { + var legend = new LinkedHashMap(); + object(raw.get("legend")).forEach((key, value) -> legend.put(key, string(value))); + answer.type(DecisionQuestionType.SCORE).score(number(raw.get("score"))) + .probabilities(probabilities(raw.get("probabilities"))) + .confidence(number(raw.get("confidence"))).legend(legend); + } + default -> throw new IllegalStateException("Unsupported TypeSafe answer type"); + } + return answer.build(); + } + + private static Map probabilities(Object raw) { + var result = new LinkedHashMap(); + object(raw).forEach((key, value) -> result.put(key, number(value))); + return result; + } + + @SuppressWarnings("unchecked") + private static Map object(Object raw) { + if (!(raw instanceof Map map) || map.keySet().stream().anyMatch(k -> !(k instanceof String))) { + throw new IllegalStateException("Expected an object in TypeSafe response"); + } + return (Map) map; + } + + private static String string(Object raw) { + if (!(raw instanceof String value) || value.isBlank()) { + throw new IllegalStateException("Expected a non-empty string in TypeSafe response"); + } + return value; + } + + private static Double number(Object raw) { + if (!(raw instanceof Number value) || !Double.isFinite(value.doubleValue())) { + throw new IllegalStateException("Expected a finite number in TypeSafe response"); + } + return value.doubleValue(); + } + + private static Integer tokens(Object raw) { + if (raw == null) { + return null; + } + var count = number(raw); + if (count < 0 || count > Integer.MAX_VALUE || count != Math.rint(count)) { + throw new IllegalStateException("Invalid token count in TypeSafe response"); + } + return count.intValue(); + } +} diff --git a/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java new file mode 100644 index 00000000..b285b14e --- /dev/null +++ b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java @@ -0,0 +1,131 @@ +package run.halo.aifoundation.provider.typesafe; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import run.halo.aifoundation.capability.DecisionCapability; +import run.halo.aifoundation.decision.DecisionQuestionType; +import run.halo.aifoundation.extension.AiProvider; +import run.halo.aifoundation.provider.AbstractAiProviderType; +import run.halo.aifoundation.provider.support.AdapterType; +import run.halo.aifoundation.provider.support.DiscoveredModel; +import run.halo.aifoundation.provider.support.DiscoveryConfidence; +import run.halo.aifoundation.provider.support.DiscoverySource; +import run.halo.aifoundation.provider.support.ModelType; +import run.halo.aifoundation.provider.support.ProviderDecisionClient; +import run.halo.aifoundation.provider.support.ProviderModelRef; +import run.halo.aifoundation.provider.transport.ProviderDiagnostics; +import run.halo.aifoundation.provider.transport.ProviderHttpResponseSupport; + +@Component +public class TypeSafeProvider extends AbstractAiProviderType { + @Override + public DecisionCapability decisionCapabilities() { + return new DecisionCapability( + List.of(DecisionQuestionType.values()), true, 255, 10); + } + + @Override + public String getProviderType() { + return "typesafe"; + } + + @Override + public String getDisplayName() { + return "TypeSafe (Jev)"; + } + + @Override + public String getDescription() { + return "Jev 决策模型,支持文本与 JSON 状态的概率判断、单选和评分。"; + } + + @Override + public String getWebsiteUrl() { + return "https://typesafe.ai"; + } + + @Override + public String getDocumentationUrl() { + return "https://docs.typesafe.ai/api"; + } + + @Override + public boolean isBuiltIn() { + return true; + } + + @Override + public boolean requiresBaseUrl() { + return false; + } + + @Override + public String getDefaultBaseUrl() { + return "https://api.typesafe.ai"; + } + + @Override + public String getCompletionsPath() { + return null; + } + + @Override + public List getSupportedAdapterTypes() { + return List.of(AdapterType.TYPESAFE_SYSTEM_ONE); + } + + @Override + public ChatModel buildChatModel(AiProvider provider, String apiKey, String modelId) { + throw new UnsupportedOperationException("TypeSafe provides decision models"); + } + + @Override + public ProviderDecisionClient buildDecisionClient(AiProvider provider, String apiKey, + ProviderModelRef model) { + var resolved = resolveModel(model); + validateNativeModelOptions(resolved); + return new TypeSafeDecisionClient(resolveBaseUrl(provider), apiKey, resolved.modelId(), + discoveryWebClientBuilder(provider)); + } + + @Override + public void validateNativeModelOptions(ProviderModelRef model) { + if (model.nativeOptions() != null && !model.nativeOptions().isEmpty()) { + throw new IllegalArgumentException("TypeSafe decision models do not accept native options"); + } + } + + @Override + public Mono> discoverModels(AiProvider provider, String apiKey) { + var endpoint = resolveBaseUrl(provider).replaceAll("/+$", "") + "/v1/models"; + var diagnostics = ProviderDiagnostics.create(getProviderType(), "models"); + return discoveryWebClientBuilder(provider).build().get().uri(endpoint) + .headers(headers -> headers.setBearerAuth(apiKey)) + .exchangeToMono(response -> { + if (!response.statusCode().is2xxSuccessful()) { + return ProviderHttpResponseSupport.errorMono(response, getProviderType(), + "models", diagnostics); + } + return response.bodyToMono(new ParameterizedTypeReference>() { }); + }) + .switchIfEmpty(Mono.error(new IllegalStateException("Empty TypeSafe model catalog"))) + .map(body -> { + if (!(body.get("models") instanceof List models)) { + throw new IllegalStateException("TypeSafe model catalog is missing models"); + } + return models.stream().map(entry -> { + if (!(entry instanceof Map item) + || !(item.get("name") instanceof String name) || name.isBlank()) { + throw new IllegalStateException("Invalid TypeSafe model catalog entry"); + } + return new DiscoveredModel(name, name, ModelType.DECISION, Set.of(), + AdapterType.TYPESAFE_SYSTEM_ONE, DiscoverySource.REMOTE, DiscoveryConfidence.HIGH); + }).toList(); + }); + } +} diff --git a/app/src/main/java/run/halo/aifoundation/service/AiModelResolver.java b/app/src/main/java/run/halo/aifoundation/service/AiModelResolver.java index bc9e9cee..a4517d76 100644 --- a/app/src/main/java/run/halo/aifoundation/service/AiModelResolver.java +++ b/app/src/main/java/run/halo/aifoundation/service/AiModelResolver.java @@ -8,6 +8,8 @@ public interface AiModelResolver { Mono resolve(String modelName, ModelType expectedType); + Mono defaultDecisionModelName(); + Mono defaultLanguageModelName(); Mono defaultEmbeddingModelName(); diff --git a/app/src/main/java/run/halo/aifoundation/service/AiModelServiceImpl.java b/app/src/main/java/run/halo/aifoundation/service/AiModelServiceImpl.java index d9bc6d22..1e9ac13b 100644 --- a/app/src/main/java/run/halo/aifoundation/service/AiModelServiceImpl.java +++ b/app/src/main/java/run/halo/aifoundation/service/AiModelServiceImpl.java @@ -6,20 +6,23 @@ import reactor.core.publisher.Mono; import run.halo.aifoundation.AiModelService; import run.halo.aifoundation.chat.LanguageModel; +import run.halo.aifoundation.decision.DecisionModel; import run.halo.aifoundation.embedding.EmbeddingModel; import run.halo.aifoundation.image.ImageGenerationModel; import run.halo.aifoundation.provider.support.ModelType; import run.halo.aifoundation.rerank.RerankingModel; +import run.halo.aifoundation.service.audit.AuditedDecisionModel; import run.halo.aifoundation.service.audit.AuditedEmbeddingModel; import run.halo.aifoundation.service.audit.AuditedImageGenerationModel; import run.halo.aifoundation.service.audit.AuditedLanguageModel; import run.halo.aifoundation.service.audit.AuditedRerankingModel; import run.halo.aifoundation.service.audit.CallerPluginAuditRecorder; import run.halo.aifoundation.service.audit.ModelCallContext; +import run.halo.aifoundation.service.decision.DecisionModelFactory; import run.halo.aifoundation.service.image.ImageGenerationModelFactory; import run.halo.aifoundation.service.model.ModelResolution; -import run.halo.aifoundation.service.rerank.RerankingModelFactory; import run.halo.aifoundation.service.observation.UsageObservation; +import run.halo.aifoundation.service.rerank.RerankingModelFactory; @Component @RequiredArgsConstructor @@ -32,6 +35,20 @@ public class AiModelServiceImpl implements AiModelService { private final ImageGenerationModelFactory imageGenerationModelFactory; private final CallerPluginAuditRecorder callerPluginAuditRecorder; private final UsageObservation usageStatistics; + private final DecisionModelFactory decisionModelFactory; + + @Override + public Mono decisionModel(String modelName) { + var name = StringUtils.hasText(modelName) + ? Mono.just(modelName) : modelResolver.defaultDecisionModelName(); + return name.flatMap(value -> modelResolver.resolve(value, ModelType.DECISION)) + .map(resolution -> { + var context = ModelCallContext.from(resolution, ModelType.DECISION); + callerPluginAuditRecorder.recordModelResolution(context); + return new AuditedDecisionModel(decisionModelFactory.create(resolution), context, + callerPluginAuditRecorder, usageStatistics); + }); + } @Override public Mono languageModel() { diff --git a/app/src/main/java/run/halo/aifoundation/service/audit/AuditedDecisionModel.java b/app/src/main/java/run/halo/aifoundation/service/audit/AuditedDecisionModel.java new file mode 100644 index 00000000..174ced25 --- /dev/null +++ b/app/src/main/java/run/halo/aifoundation/service/audit/AuditedDecisionModel.java @@ -0,0 +1,31 @@ +package run.halo.aifoundation.service.audit; + +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Mono; +import run.halo.aifoundation.capability.DecisionCapability; +import run.halo.aifoundation.decision.DecisionModel; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.decision.DecisionResponse; +import run.halo.aifoundation.service.observation.UsageObservation; +import run.halo.aifoundation.service.observation.UsageOperation; + +@RequiredArgsConstructor +public class AuditedDecisionModel implements DecisionModel { + private final DecisionModel delegate; + private final ModelCallContext context; + private final CallerPluginAuditRecorder auditRecorder; + private final UsageObservation usageStatistics; + + @Override + public DecisionCapability capabilities() { + return delegate.capabilities(); + } + + @Override + public Mono decide(DecisionRequest request) { + auditRecorder.recordModelInvocation(context, UsageOperation.DECISION); + return UsageCallRecorder.record(usageStatistics, context, UsageOperation.DECISION, false, + request == null ? null : request.getMetadata(), () -> delegate.decide(request), 1, + (session, response) -> session.succeed(response.getModel(), 1)); + } +} diff --git a/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityMatcher.java b/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityMatcher.java index 2b126c88..d2a7e009 100644 --- a/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityMatcher.java +++ b/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityMatcher.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; +import run.halo.aifoundation.capability.DecisionCapability; import run.halo.aifoundation.capability.ImageGenerationCapability; import run.halo.aifoundation.capability.InputSource; import run.halo.aifoundation.capability.LanguageCapability; @@ -18,6 +19,8 @@ public CapabilityMatchResult match(ModelCapabilities capabilities, return CapabilityMatchResult.success(); } var issues = new ArrayList(); + matchDecision(capabilities == null ? null : capabilities.getDecision(), + requirement.getDecision(), issues); matchLanguage(capabilities == null ? null : capabilities.getLanguage(), requirement.getLanguage(), issues); matchImageGeneration(capabilities == null ? null : capabilities.getImageGeneration(), @@ -47,6 +50,25 @@ private void matchLanguage(LanguageCapability actual, LanguageCapability expecte expected.getInputSources(), issues); } + private void matchDecision(DecisionCapability actual, + DecisionCapability expected, List issues) { + if (expected == null) { + return; + } + matchBoolean("decision.structuredState", actual == null ? null : actual.getStructuredState(), + expected.getStructuredState(), issues); + matchMinInteger("decision.maxChoiceOptions", actual == null ? null : actual.getMaxChoiceOptions(), + expected.getMaxChoiceOptions(), issues); + matchMinInteger("decision.maxScoreLevels", actual == null ? null : actual.getMaxScoreLevels(), + expected.getMaxScoreLevels(), issues); + if (expected.getQuestionTypes() != null && !expected.getQuestionTypes().isEmpty() + && (actual == null || actual.getQuestionTypes() == null + || !actual.getQuestionTypes().containsAll(expected.getQuestionTypes()))) { + issues.add(new CapabilityMatchIssue("decision.questionTypes", expected.getQuestionTypes(), + actual == null ? null : actual.getQuestionTypes())); + } + } + private void matchImageGeneration(ImageGenerationCapability actual, ImageGenerationCapability expected, List issues) { if (expected == null) { diff --git a/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityService.java b/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityService.java index ffa9c0d5..73dd6bbb 100644 --- a/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityService.java +++ b/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityService.java @@ -28,6 +28,10 @@ public ModelCapabilities effectiveCapabilities(AiModel model, var spec = model.getSpec(); var explicit = spec.getCapabilities(); var sources = effectiveSources(explicit, spec.getCapabilitySources()); + var decision = spec.getModelType() == ModelType.DECISION && providerType != null + ? providerType.decisionCapabilities() : null; + // Decision limits are adapter-owned and cannot be widened by persisted model metadata. + sources.setDecision(decision == null ? CapabilitySource.UNKNOWN : CapabilitySource.BUILT_IN); var language = copy(explicit == null ? null : explicit.getLanguage()); var imageGeneration = copy(explicit == null ? null : explicit.getImageGeneration()); @@ -40,6 +44,7 @@ public ModelCapabilities effectiveCapabilities(AiModel model, providerType); } return ModelCapabilities.builder() + .decision(decision) .language(language) .imageGeneration(imageGeneration) .sources(sources) diff --git a/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelFactory.java b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelFactory.java new file mode 100644 index 00000000..9110f2ef --- /dev/null +++ b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelFactory.java @@ -0,0 +1,26 @@ +package run.halo.aifoundation.service.decision; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import run.halo.aifoundation.decision.DecisionModel; +import run.halo.aifoundation.exception.ModelNotFoundException; +import run.halo.aifoundation.provider.support.ProviderClientCache; +import run.halo.aifoundation.provider.support.ProviderModelRef; +import run.halo.aifoundation.service.model.ModelResolution; +import run.halo.aifoundation.service.observation.UsageExecutionObserver; + +@Component +@RequiredArgsConstructor +public class DecisionModelFactory { + private final ProviderClientCache providerClientCache; + private final UsageExecutionObserver observer; + + public DecisionModel create(ModelResolution resolution) { + var client = providerClientCache.getOrCreateDecisionClient(resolution.provider(), + resolution.apiKey(), ProviderModelRef.from(resolution.model())); + if (client == null) { + throw new ModelNotFoundException("Provider does not support decision inference"); + } + return new DecisionModelImpl(client, observer, resolution.providerType().decisionCapabilities()); + } +} diff --git a/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java new file mode 100644 index 00000000..0ec3466c --- /dev/null +++ b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java @@ -0,0 +1,67 @@ +package run.halo.aifoundation.service.decision; + +import java.util.List; +import java.util.concurrent.TimeoutException; +import reactor.core.publisher.Mono; +import run.halo.aifoundation.capability.DecisionCapability; +import run.halo.aifoundation.decision.DecisionModel; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.decision.DecisionResponse; +import run.halo.aifoundation.exception.DecisionCancelledException; +import run.halo.aifoundation.exception.DecisionTimeoutException; +import run.halo.aifoundation.provider.support.ProviderDecisionClient; +import run.halo.aifoundation.service.observation.NormalizedUsage; +import run.halo.aifoundation.service.observation.UsageExecutionObserver; +import run.halo.aifoundation.service.observation.UsageUnitKind; + +public class DecisionModelImpl implements DecisionModel { + private final ProviderDecisionClient client; + private final UsageExecutionObserver observer; + private final DecisionCapability capabilities; + + public DecisionModelImpl(ProviderDecisionClient client, UsageExecutionObserver observer, + DecisionCapability capabilities) { + this.client = client; + this.observer = observer; + this.capabilities = copyCapabilities(capabilities); + } + + @Override + public DecisionCapability capabilities() { + return copyCapabilities(capabilities); + } + + private static DecisionCapability copyCapabilities(DecisionCapability capabilities) { + return capabilities == null ? null : new DecisionCapability( + capabilities.getQuestionTypes() == null ? null + : List.copyOf(capabilities.getQuestionTypes()), + capabilities.getStructuredState(), capabilities.getMaxChoiceOptions(), + capabilities.getMaxScoreLevels()); + } + + @Override + public Mono decide(DecisionRequest request) { + return Mono.defer(() -> { + DecisionValidation.request(request); + DecisionValidation.capabilities(request, capabilities); + checkCancellation(request); + var call = observer.observe(UsageUnitKind.DECISION, 0, + () -> client.decide(request) + .switchIfEmpty(Mono.error(new IllegalStateException("Empty decision response"))) + .doOnNext(response -> DecisionValidation.response(request, response)), + response -> NormalizedUsage.from(response.getUsage()), DecisionResponse::getModel) + .doOnNext(response -> checkCancellation(request)); + var timeout = request.getTimeouts() == null ? null + : request.getTimeouts().getTotalTimeout(); + return timeout == null ? call : call.timeout(timeout) + .onErrorMap(TimeoutException.class, error -> new DecisionTimeoutException(timeout, error)); + }); + } + + private static void checkCancellation(DecisionRequest request) { + if (request.getCancellationToken() != null + && request.getCancellationToken().isCancellationRequested()) { + throw new DecisionCancelledException(); + } + } +} diff --git a/app/src/main/java/run/halo/aifoundation/service/decision/DecisionValidation.java b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionValidation.java new file mode 100644 index 00000000..a51d1283 --- /dev/null +++ b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionValidation.java @@ -0,0 +1,175 @@ +package run.halo.aifoundation.service.decision; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import run.halo.aifoundation.capability.DecisionCapability; +import run.halo.aifoundation.decision.DecisionAnswer; +import run.halo.aifoundation.decision.DecisionQuestion; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.decision.DecisionResponse; + +/** Validates neutral request/response semantics independently of any provider. */ +public final class DecisionValidation { + private DecisionValidation() { + } + + public static void request(DecisionRequest request) { + require(request != null, "Decision request is required"); + var state = request.getState(); + require(state instanceof String || state instanceof Map || state instanceof List, + "Decision state must be text, a JSON object, or a JSON array"); + validateJson(state, 0); + require(!(state instanceof String text) || !text.isBlank(), "Decision state is required"); + require(request.getQuestions() != null && !request.getQuestions().isEmpty(), + "At least one decision question is required"); + request.getQuestions().forEach((id, question) -> { + require(id != null && !id.isBlank(), "Question IDs must not be blank"); + question(question); + }); + var timeout = request.getTimeouts() == null ? null + : request.getTimeouts().getTotalTimeout(); + require(timeout == null || (!timeout.isZero() && !timeout.isNegative()), + "Decision timeout must be positive"); + } + + private static void validateJson(Object value, int depth) { + require(depth <= 32, "Decision state exceeds the supported JSON nesting depth"); + if (value == null || value instanceof String || value instanceof Boolean) { + return; + } + if (value instanceof Number number) { + require(Double.isFinite(number.doubleValue()), "State numbers must be finite"); + return; + } + if (value instanceof Map map) { + map.forEach((key, child) -> { + require(key instanceof String, "State object keys must be strings"); + validateJson(child, depth + 1); + }); + return; + } + if (value instanceof List list) { + list.forEach(child -> validateJson(child, depth + 1)); + return; + } + throw new IllegalArgumentException("Decision state must contain only JSON values"); + } + + public static void capabilities(DecisionRequest request, + DecisionCapability capabilities) { + require(capabilities != null && capabilities.getQuestionTypes() != null, + "Decision capabilities are unavailable"); + require(request.getState() instanceof String || Boolean.TRUE.equals(capabilities.getStructuredState()), + "This decision model does not support structured state"); + request.getQuestions().values().forEach(question -> { + require(capabilities.getQuestionTypes().contains(question.getType()), + "This decision model does not support question type " + question.getType()); + if (question.getOptions() != null && capabilities.getMaxChoiceOptions() != null) { + require(question.getOptions().size() <= capabilities.getMaxChoiceOptions(), + "Too many choice options; maximum is " + capabilities.getMaxChoiceOptions()); + } + if (question.getLevels() != null && capabilities.getMaxScoreLevels() != null) { + require(question.getLevels().size() <= capabilities.getMaxScoreLevels(), + "Too many score levels; maximum is " + capabilities.getMaxScoreLevels()); + } + }); + } + + private static void question(DecisionQuestion question) { + require(question != null && question.getType() != null, "Question type is required"); + require(question.getInstructions() != null && !question.getInstructions().isBlank(), + "Question instructions are required"); + switch (question.getType()) { + case YES_NO -> require(question.getOptions() == null && question.getLevels() == null, + "Yes/no questions do not accept options or levels"); + case CHOICE -> { + require(question.getOptions() != null && question.getOptions().size() >= 2, + "Choice questions require at least two options"); + question.getOptions().forEach((key, description) -> { + require(key != null && !key.isBlank(), "Choice option IDs must not be blank"); + require(description != null, "Choice option descriptions must not be null"); + }); + require(question.getLevels() == null, "Choice questions do not accept levels"); + noBooleanCriteria(question); + } + case SCORE -> { + require(question.getLevels() != null && question.getLevels().size() >= 2, + "Score questions require at least two ordered levels"); + question.getLevels().forEach(level -> require(level != null && !level.isBlank(), + "Score level descriptions must not be blank")); + require(question.getOptions() == null, "Score questions do not accept options"); + noBooleanCriteria(question); + } + } + } + + private static void noBooleanCriteria(DecisionQuestion question) { + require(question.getYesCriteria() == null && question.getNoCriteria() == null, + "Only yes/no questions accept yesCriteria or noCriteria"); + } + + public static void response(DecisionRequest request, DecisionResponse response) { + valid(response != null && response.getModel() != null && !response.getModel().isBlank(), + "Missing decision response model"); + valid(response.getAnswers() != null + && response.getAnswers().keySet().equals(request.getQuestions().keySet()), + "Decision response question IDs do not match the request"); + request.getQuestions().forEach((id, question) -> { + var answer = response.getAnswers().get(id); + valid(answer != null && answer.getType() == question.getType(), + "Decision response type does not match question " + id); + switch (question.getType()) { + case YES_NO -> probability(answer.getProbability()); + case CHOICE -> { + distribution(answer, question.getOptions().keySet()); + valid(answer.getChoice() != null + && question.getOptions().containsKey(answer.getChoice()), + "Decision response selected an unknown option"); + } + case SCORE -> { + var keys = IntStream.range(0, question.getLevels().size()) + .mapToObj(Integer::toString).collect(Collectors.toCollection(LinkedHashSet::new)); + distribution(answer, keys); + var score = answer.getScore(); + valid(score != null && Double.isFinite(score) && score >= 0 + && score <= question.getLevels().size() - 1, + "Decision score is outside the requested levels"); + valid(answer.getLegend() != null && answer.getLegend().keySet().equals(keys) + && answer.getLegend().values().stream().allMatch(v -> v != null), + "Decision score legend does not match the requested levels"); + } + } + }); + } + + private static void distribution(DecisionAnswer answer, Set keys) { + var probabilities = answer.getProbabilities(); + valid(probabilities != null && probabilities.keySet().equals(keys), + "Decision probabilities do not match the requested options"); + probabilities.values().forEach(DecisionValidation::probability); + valid(Math.abs(probabilities.values().stream().mapToDouble(Double::doubleValue).sum() - 1) + <= 0.001, "Decision probabilities must sum to one"); + probability(answer.getConfidence()); + } + + private static void probability(Double value) { + valid(value != null && Double.isFinite(value) && value >= 0 && value <= 1, + "Decision probabilities and confidence must be finite values between zero and one"); + } + + private static void require(boolean condition, String message) { + if (!condition) { + throw new IllegalArgumentException(message); + } + } + + private static void valid(boolean condition, String message) { + if (!condition) { + throw new IllegalStateException(message); + } + } +} diff --git a/app/src/main/java/run/halo/aifoundation/service/model/DefaultAiModelResolver.java b/app/src/main/java/run/halo/aifoundation/service/model/DefaultAiModelResolver.java index 392a97f9..2b9bf224 100644 --- a/app/src/main/java/run/halo/aifoundation/service/model/DefaultAiModelResolver.java +++ b/app/src/main/java/run/halo/aifoundation/service/model/DefaultAiModelResolver.java @@ -55,6 +55,11 @@ private Mono resolveProvider(AiModel model, AiProvider provider providerClientCache.getProviderType(provider.getSpec().getProviderType()), apiKey)); } + @Override + public Mono defaultDecisionModelName() { + return defaultSlotName("decision", DefaultModelSlots::getDecisionModelName); + } + @Override public Mono defaultLanguageModelName() { return defaultSlotName("language", DefaultModelSlots::getLanguageModelName); diff --git a/app/src/main/java/run/halo/aifoundation/service/observation/NormalizedUsage.java b/app/src/main/java/run/halo/aifoundation/service/observation/NormalizedUsage.java index 2095bbda..eda795e0 100644 --- a/app/src/main/java/run/halo/aifoundation/service/observation/NormalizedUsage.java +++ b/app/src/main/java/run/halo/aifoundation/service/observation/NormalizedUsage.java @@ -4,6 +4,7 @@ import org.springframework.ai.chat.metadata.Usage; import org.springframework.util.CollectionUtils; import run.halo.aifoundation.chat.LanguageModelUsage; +import run.halo.aifoundation.decision.DecisionUsage; import run.halo.aifoundation.embedding.EmbeddingUsage; import run.halo.aifoundation.image.ImageUsage; import run.halo.aifoundation.provider.usage.ProviderUsage; @@ -72,6 +73,12 @@ public static NormalizedUsage from(EmbeddingUsage usage) { null, null); } + public static NormalizedUsage from(DecisionUsage usage) { + return usage == null ? missing() + : new NormalizedUsage(value(usage.getInputTokens()), value(usage.getOutputTokens()), + null, null, null, null, null, null); + } + public static NormalizedUsage from(RerankUsage usage) { return usage == null ? missing() : new NormalizedUsage(value(usage.getInputTokens()), null, null, null, null, diff --git a/app/src/main/java/run/halo/aifoundation/service/observation/UsageError.java b/app/src/main/java/run/halo/aifoundation/service/observation/UsageError.java index dc93af70..b2f37107 100644 --- a/app/src/main/java/run/halo/aifoundation/service/observation/UsageError.java +++ b/app/src/main/java/run/halo/aifoundation/service/observation/UsageError.java @@ -9,6 +9,8 @@ import reactor.core.Exceptions; import run.halo.aifoundation.exception.AiGenerationCancelledException; import run.halo.aifoundation.exception.AiGenerationTimeoutException; +import run.halo.aifoundation.exception.DecisionCancelledException; +import run.halo.aifoundation.exception.DecisionTimeoutException; import run.halo.aifoundation.exception.EmbeddingCancelledException; import run.halo.aifoundation.exception.EmbeddingTimeoutException; import run.halo.aifoundation.exception.RerankCancelledException; @@ -49,6 +51,7 @@ private static boolean isTimeoutType(Throwable error) { case AiGenerationTimeoutException ignored -> true; case EmbeddingTimeoutException ignored -> true; case RerankTimeoutException ignored -> true; + case DecisionTimeoutException ignored -> true; case TimeoutException ignored -> true; case SocketTimeoutException ignored -> true; case HttpTimeoutException ignored -> true; @@ -66,6 +69,7 @@ private static boolean isCancellationType(Throwable error) { case AiGenerationCancelledException ignored -> true; case EmbeddingCancelledException ignored -> true; case RerankCancelledException ignored -> true; + case DecisionCancelledException ignored -> true; default -> false; }; } diff --git a/app/src/main/java/run/halo/aifoundation/service/observation/UsageOperation.java b/app/src/main/java/run/halo/aifoundation/service/observation/UsageOperation.java index 0c9eb2c9..83816420 100644 --- a/app/src/main/java/run/halo/aifoundation/service/observation/UsageOperation.java +++ b/app/src/main/java/run/halo/aifoundation/service/observation/UsageOperation.java @@ -8,6 +8,7 @@ public final class UsageOperation { public static final String EMBEDDING_EMBED = "embedding.embed"; public static final String EMBEDDING_EMBED_QUERY = "embedding.embedQuery"; public static final String IMAGE_GENERATE_IMAGE = "image.generateImage"; + public static final String DECISION = "decision.decide"; public static final String RERANK = "rerank.rerank"; private UsageOperation() { diff --git a/app/src/main/java/run/halo/aifoundation/service/observation/UsageUnitKind.java b/app/src/main/java/run/halo/aifoundation/service/observation/UsageUnitKind.java index b7657890..38733998 100644 --- a/app/src/main/java/run/halo/aifoundation/service/observation/UsageUnitKind.java +++ b/app/src/main/java/run/halo/aifoundation/service/observation/UsageUnitKind.java @@ -4,5 +4,6 @@ public enum UsageUnitKind { GENERATION_STEP, EMBEDDING_BATCH, RERANK, - IMAGE_BATCH + IMAGE_BATCH, + DECISION } diff --git a/app/src/main/java/run/halo/aifoundation/setting/DefaultModelSlots.java b/app/src/main/java/run/halo/aifoundation/setting/DefaultModelSlots.java index 47a0b054..00073264 100644 --- a/app/src/main/java/run/halo/aifoundation/setting/DefaultModelSlots.java +++ b/app/src/main/java/run/halo/aifoundation/setting/DefaultModelSlots.java @@ -7,6 +7,8 @@ public class DefaultModelSlots { public static final String GROUP = "defaults"; + private String decisionModelName; + private String languageModelName; private String embeddingModelName; diff --git a/app/src/test/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpointTest.java b/app/src/test/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpointTest.java new file mode 100644 index 00000000..667a1443 --- /dev/null +++ b/app/src/test/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpointTest.java @@ -0,0 +1,48 @@ +package run.halo.aifoundation.endpoint; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; +import reactor.core.publisher.Mono; +import run.halo.aifoundation.AiModelService; +import run.halo.aifoundation.decision.DecisionModel; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.decision.DecisionResponse; + +class DecisionConsoleEndpointTest { + private final AiModelService service = mock(AiModelService.class); + private final WebTestClient client = WebTestClient.bindToRouterFunction( + new DecisionConsoleEndpoint(service).endpoint()).build(); + + @Test + void invalidQuestionsAreRejectedBeforeResolvingModel() { + client.post().uri("/models/configured-model/test-decision").contentType(MediaType.APPLICATION_JSON) + .bodyValue("{\"state\":\"text\",\"questions\":{}}") + .exchange().expectStatus().isBadRequest(); + verifyNoInteractions(service); + } + + @Test + void resolvesResourceNameAndSuppliesLocalAuditMetadata() { + var model = mock(DecisionModel.class); + when(service.decisionModel("configured-model")).thenReturn(Mono.just(model)); + when(model.decide(any())).thenReturn(Mono.just(new DecisionResponse("jev-version", Map.of(), null))); + client.post().uri("/models/configured-model/test-decision").contentType(MediaType.APPLICATION_JSON) + .bodyValue(Map.of("state", "{\"comment\":\"hello\"}", "stateFormat", "JSON", + "questions", Map.of("q", Map.of("type", "YES_NO", "instructions", "Spam?")))) + .exchange().expectStatus().isOk().expectBody().jsonPath("$.model").isEqualTo("jev-version"); + var request = ArgumentCaptor.forClass(DecisionRequest.class); + verify(model).decide(request.capture()); + assertThat(request.getValue().getState()).isEqualTo(Map.of("comment", "hello")); + assertThat(request.getValue().getMetadata()).containsEntry("source", "console-test"); + } +} diff --git a/app/src/test/java/run/halo/aifoundation/endpoint/ModelOptionConsoleEndpointTest.java b/app/src/test/java/run/halo/aifoundation/endpoint/ModelOptionConsoleEndpointTest.java index ed984d30..8eb47b6a 100644 --- a/app/src/test/java/run/halo/aifoundation/endpoint/ModelOptionConsoleEndpointTest.java +++ b/app/src/test/java/run/halo/aifoundation/endpoint/ModelOptionConsoleEndpointTest.java @@ -22,6 +22,7 @@ import run.halo.aifoundation.provider.support.ModelFeature; import run.halo.aifoundation.provider.support.ModelType; import run.halo.aifoundation.provider.support.ProviderClientCache; +import run.halo.aifoundation.provider.typesafe.TypeSafeProvider; import run.halo.aifoundation.service.capability.ModelCapabilityMatcher; import run.halo.aifoundation.service.capability.ModelCapabilityService; import run.halo.app.extension.Metadata; @@ -324,6 +325,34 @@ private URI capabilitiesUri(String prefixQuery, String requiredCapabilities) { return URI.create("/model-options?" + query); } + @Test + void decisionOptionsExposeAdapterLimitsAndFilterRequiredCapabilities() { + when(providerClientCache.getProviderTypeMap()) + .thenReturn(Map.of("typesafe", new TypeSafeProvider())); + mockData(List.of(model("decision-resource", "typesafe-prod", "jev-latest", "Jev", + ModelType.DECISION, true, List.of())), + List.of(provider("typesafe-prod", "TypeSafe", "typesafe", true, + AiProvider.AiProviderStatus.Phase.UNKNOWN))); + + webTestClient.get().uri(capabilitiesUri("modelType=decision", """ + {"decision":{"questionTypes":["YES_NO","SCORE"],"structuredState":true,"maxScoreLevels":10}} + """)) + .exchange().expectStatus().isOk().expectBody() + .jsonPath("$[0].name").isEqualTo("decision-resource") + .jsonPath("$[0].capabilities.decision.maxChoiceOptions").isEqualTo(255) + .jsonPath("$[0].capabilities.sources.decision").isEqualTo("built-in"); + + webTestClient.get().uri(capabilitiesUri(""" + {"decision":{"maxScoreLevels":11}} + """)) + .exchange().expectStatus().isOk().expectBody().json("[]"); + + webTestClient.get().uri(capabilitiesUri(""" + {"decision":{"questionTypes":["BOX"]}} + """)) + .exchange().expectStatus().isBadRequest(); + } + private ModelOption option(List options, String name) { return options.stream() .filter(option -> name.equals(option.getName())) diff --git a/app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java b/app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java new file mode 100644 index 00000000..5d15dac3 --- /dev/null +++ b/app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java @@ -0,0 +1,143 @@ +package run.halo.aifoundation.provider.typesafe; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sun.net.httpserver.HttpServer; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.test.StepVerifier; +import run.halo.aifoundation.decision.DecisionQuestion; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.extension.AiProvider; +import run.halo.aifoundation.provider.support.ModelType; +import run.halo.aifoundation.provider.transport.ProviderHttpException; +import run.halo.aifoundation.service.decision.DecisionModelImpl; +import run.halo.aifoundation.service.observation.UsageExecutionObserver; +import tools.jackson.databind.json.JsonMapper; + +class TypeSafeDecisionClientTest { + private HttpServer server; + private String baseUrl; + private final AtomicReference response = new AtomicReference<>("{}"); + private final AtomicReference requestBody = new AtomicReference<>(); + private final AtomicReference path = new AtomicReference<>(); + private final AtomicReference authorization = new AtomicReference<>(); + private final AtomicInteger status = new AtomicInteger(200); + + @BeforeEach + void start() throws Exception { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + path.set(exchange.getRequestURI().getPath()); + authorization.set(exchange.getRequestHeaders().getFirst("Authorization")); + requestBody.set(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)); + var bytes = response.get().getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(status.get(), bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + }); + server.start(); + baseUrl = "http://127.0.0.1:" + server.getAddress().getPort(); + } + + @AfterEach + void stop() { + server.stop(0); + } + + @Test + void evaluatesMixedQuestionsAndPreservesProbabilitiesAndFractionalScore() { + response.set(""" + {"model":"jev-1.14.0","answers":{ + "spam":{"type":"noul","noul":0.12}, + "route":{"type":"choice","choice":"review", + "probabilities":{"review":0.8,"publish":0.2},"confidence":0.6}, + "severity":{"type":"score","score":0.25,"legend":{"0":"low","1":"high"}, + "probabilities":{"0":0.75,"1":0.25},"confidence":0.5}}, + "usage":{"input_tokens":123,"output_tokens":22}} + """); + var request = DecisionRequest.builder().state(Map.of("comment", "please review")) + .questions(Map.of("spam", DecisionQuestion.yesNo("Is this spam?"), + "route", DecisionQuestion.choice("What next?", Map.of("review", "Review", "publish", "Publish")), + "severity", DecisionQuestion.score("How severe?", List.of("low", "high")))) + .metadata(Map.of("private", "local-only")).build(); + var client = client(); + var runtime = new DecisionModelImpl(client, new UsageExecutionObserver(), + new TypeSafeProvider().decisionCapabilities()); + StepVerifier.create(runtime.decide(request)).assertNext(result -> { + assertThat(result.getModel()).isEqualTo("jev-1.14.0"); + assertThat(result.getAnswers().get("spam").getProbability()).isEqualTo(0.12); + assertThat(result.getAnswers().get("spam").getConfidence()).isNull(); + assertThat(result.getAnswers().get("route").getConfidence()).isEqualTo(0.6); + assertThat(result.getAnswers().get("route").getProbabilities()).containsEntry("review", 0.8); + assertThat(result.getAnswers().get("severity").getScore()).isEqualTo(0.25); + assertThat(result.getUsage().getInputTokens()).isEqualTo(123); + assertThat(result.getUsage().getOutputTokens()).isEqualTo(22); + }).verifyComplete(); + assertThat(path.get()).isEqualTo("/v1/systemone"); + assertThat(authorization.get()).isEqualTo("Bearer test-key"); + var body = JsonMapper.builder().build().readTree(requestBody.get()); + assertThat(body.path("model").asText()).isEqualTo("jev-latest"); + assertThat(body.path("questions").path("spam").path("type").asText()).isEqualTo("noul"); + assertThat(body.path("questions").path("route").path("criteria").path("review").asText()).isEqualTo("Review"); + assertThat(body.path("questions").path("severity").path("criteria").size()).isEqualTo(2); + assertThat(requestBody.get()).doesNotContain("local-only", "metadata", "messages"); + } + + @Test + void discoversModelsUsingTypeSafeCatalogShape() { + response.set(""" + {"models":[{"name":"jev-latest","description":"Decision model","release_date":"2026-09-15"}]} + """); + var provider = new AiProvider(); + provider.setSpec(new AiProvider.AiProviderSpec()); + provider.getSpec().setBaseUrl(baseUrl + "/"); + StepVerifier.create(new TypeSafeProvider().discoverModels(provider, "test-key")) + .assertNext(models -> { + assertThat(models).hasSize(1); + assertThat(models.getFirst().modelId()).isEqualTo("jev-latest"); + assertThat(models.getFirst().modelType()).isEqualTo(ModelType.DECISION); + }).verifyComplete(); + assertThat(path.get()).isEqualTo("/v1/models"); + assertThat(authorization.get()).isEqualTo("Bearer test-key"); + } + + @Test + void preservesMissingUsageAsUnknownAndMapsYesNoCriteria() { + response.set(""" + {"model":"jev-version","answers":{"q":{"type":"noul","noul":0.5}},"usage":{"input_tokens":null}} + """); + var question = DecisionQuestion.yesNo("Urgent?"); + question.setYesCriteria("Time-sensitive"); + question.setNoCriteria("Can wait"); + StepVerifier.create(client().decide(DecisionRequest.builder().state("message") + .questions(Map.of("q", question)).build())) + .assertNext(result -> { + assertThat(result.getUsage().getInputTokens()).isNull(); + assertThat(result.getUsage().getOutputTokens()).isNull(); + }).verifyComplete(); + assertThat(requestBody.get()).contains("\"true\":\"Time-sensitive\"", "\"false\":\"Can wait\""); + } + + @Test + void propagatesProviderRateLimitErrors() { + status.set(429); + response.set("{\"detail\":\"Rate limited\"}"); + StepVerifier.create(client().decide(DecisionRequest.builder().state("message") + .questions(Map.of("q", DecisionQuestion.yesNo("Urgent?"))).build())) + .expectError(ProviderHttpException.class).verify(); + } + + private TypeSafeDecisionClient client() { + return new TypeSafeDecisionClient(baseUrl + "/", "test-key", "jev-latest", WebClient.builder()); + } +} diff --git a/app/src/test/java/run/halo/aifoundation/service/AiModelServiceImplTest.java b/app/src/test/java/run/halo/aifoundation/service/AiModelServiceImplTest.java index 4ded9149..08f68a9b 100644 --- a/app/src/test/java/run/halo/aifoundation/service/AiModelServiceImplTest.java +++ b/app/src/test/java/run/halo/aifoundation/service/AiModelServiceImplTest.java @@ -7,12 +7,12 @@ import static org.mockito.Mockito.when; import java.util.List; -import org.springframework.ai.chat.model.ChatModel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.ai.chat.model.ChatModel; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import run.halo.aifoundation.exception.DefaultModelNotConfiguredException; @@ -31,13 +31,13 @@ import run.halo.aifoundation.provider.support.ProviderRerankingClient; import run.halo.aifoundation.provider.support.SecretResolver; import run.halo.aifoundation.rerank.RerankResponse; -import run.halo.aifoundation.service.embedding.DefaultEmbeddingModelFactory; -import run.halo.aifoundation.service.embedding.EmbeddingModelRuntimeFactory; import run.halo.aifoundation.service.audit.CallerPluginAuditRecorder; import run.halo.aifoundation.service.audit.CallerPluginInfo; import run.halo.aifoundation.service.audit.CallerPluginObservationRegistry; import run.halo.aifoundation.service.audit.CallerPluginResolver; import run.halo.aifoundation.service.capability.ModelCapabilityMatcher; +import run.halo.aifoundation.service.embedding.DefaultEmbeddingModelFactory; +import run.halo.aifoundation.service.embedding.EmbeddingModelRuntimeFactory; import run.halo.aifoundation.service.image.DefaultImageGenerationModelFactory; import run.halo.aifoundation.service.language.DefaultLanguageModelFactory; import run.halo.aifoundation.service.language.LanguageModelRuntimeFactory; @@ -45,11 +45,11 @@ import run.halo.aifoundation.service.language.LanguageModelRuntimeSupport; import run.halo.aifoundation.service.media.MediaResourcePolicy; import run.halo.aifoundation.service.model.DefaultAiModelResolver; -import run.halo.aifoundation.service.rerank.DefaultRerankingModelFactory; -import run.halo.aifoundation.service.rerank.RerankingModelRuntimeFactory; import run.halo.aifoundation.service.observation.UsageCallDescriptor; import run.halo.aifoundation.service.observation.UsageCallSession; import run.halo.aifoundation.service.observation.UsageExecutionObserver; +import run.halo.aifoundation.service.rerank.DefaultRerankingModelFactory; +import run.halo.aifoundation.service.rerank.RerankingModelRuntimeFactory; import run.halo.aifoundation.service.usage.UsageStatisticsService; import run.halo.aifoundation.setting.DefaultModelSlotStore; import run.halo.aifoundation.setting.DefaultModelSlots; @@ -112,10 +112,41 @@ void setUp() { new RerankingModelRuntimeFactory(new UsageExecutionObserver())), new DefaultImageGenerationModelFactory(), callerPluginAuditRecorder, - usageStatisticsService + usageStatisticsService, + new run.halo.aifoundation.service.decision.DecisionModelFactory(providerClientCache, + new UsageExecutionObserver()) ); } + @Test + void decisionModel_resolvesDefaultResourceNameAndKeepsProviderCapabilities() { + var slots = new DefaultModelSlots(); + slots.setDecisionModelName("decision-resource"); + var model = aiModel("decision-resource", "typesafe-prod", "jev-latest", "Jev", true, + ModelType.DECISION); + var provider = aiProvider("typesafe-prod", "typesafe", true); + when(defaultModelSlotStore.get()).thenReturn(Mono.just(slots)); + when(client.fetch(AiModel.class, "decision-resource")).thenReturn(Mono.just(model)); + when(client.fetch(AiProvider.class, "typesafe-prod")).thenReturn(Mono.just(provider)); + when(secretResolver.resolveApiKey(null)).thenReturn(Mono.just("sk-test")); + when(providerClientCache.getProviderType("typesafe")) + .thenReturn(new run.halo.aifoundation.provider.typesafe.TypeSafeProvider()); + when(providerClientCache.getOrCreateDecisionClient(provider, "sk-test", ProviderModelRef.from(model))) + .thenReturn(request -> Mono.empty()); + StepVerifier.create(service.decisionModel()) + .assertNext(resolved -> assertThat(resolved.capabilities().getMaxScoreLevels()).isEqualTo(10)) + .verifyComplete(); + verify(client).fetch(AiModel.class, "decision-resource"); + } + + @Test + void decisionModel_rejectsWrongModelType() { + when(client.fetch(AiModel.class, "language")) + .thenReturn(Mono.just(aiModel("language", "openai", "model", "Model", true))); + StepVerifier.create(service.decisionModel("language")) + .expectError(IncompatibleModelTypeException.class).verify(); + } + // ---- languageModel — fetch by metadata.name ---- @Test @@ -468,6 +499,7 @@ private AiProviderType providerType(ModelType modelType) { private AdapterType defaultAdapter(ModelType modelType) { return switch (modelType) { + case DECISION -> AdapterType.TYPESAFE_SYSTEM_ONE; case LANGUAGE -> AdapterType.OPENAI_CHAT; case EMBEDDING -> AdapterType.OPENAI_EMBEDDING; case IMAGE_GENERATION -> AdapterType.OPENAI_IMAGE; diff --git a/app/src/test/java/run/halo/aifoundation/service/audit/AuditedDecisionModelTest.java b/app/src/test/java/run/halo/aifoundation/service/audit/AuditedDecisionModelTest.java new file mode 100644 index 00000000..5429de2c --- /dev/null +++ b/app/src/test/java/run/halo/aifoundation/service/audit/AuditedDecisionModelTest.java @@ -0,0 +1,76 @@ +package run.halo.aifoundation.service.audit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Clock; +import java.time.Instant; +import java.util.Map; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; +import run.halo.aifoundation.decision.DecisionAnswer; +import run.halo.aifoundation.decision.DecisionQuestion; +import run.halo.aifoundation.decision.DecisionQuestionType; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.decision.DecisionResponse; +import run.halo.aifoundation.decision.DecisionUsage; +import run.halo.aifoundation.provider.support.ModelType; +import run.halo.aifoundation.provider.typesafe.TypeSafeProvider; +import run.halo.aifoundation.service.decision.DecisionModelImpl; +import run.halo.aifoundation.service.observation.UsageCallDescriptor; +import run.halo.aifoundation.service.observation.UsageCallSession; +import run.halo.aifoundation.service.observation.UsageCallStart; +import run.halo.aifoundation.service.observation.UsageExecutionObserver; +import run.halo.aifoundation.service.observation.UsageExecutionRecord; +import run.halo.aifoundation.service.observation.UsageQuality; +import run.halo.aifoundation.service.observation.UsageStatus; +import run.halo.aifoundation.service.observation.UsageUnitKind; +import run.halo.aifoundation.service.usage.UsageStatisticsService; + +class AuditedDecisionModelTest { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void recordsDecisionExecutionWithoutInventingMissingUsage(boolean hasUsage) { + var statistics = mock(UsageStatisticsService.class); + var audit = mock(CallerPluginAuditRecorder.class); + var context = new ModelCallContext(ModelType.DECISION, + "decision-resource", "provider", "typesafe", "jev-latest"); + var descriptor = mock(UsageCallDescriptor.class); + var metadata = Map.of("feature", "moderation"); + var start = new UsageCallStart("decision-call", 1, Instant.now(), "plugin", "1", + "stack", null, "decision.decide", "decision", "decision-resource", + "provider", "typesafe", "jev-latest", false); + var session = new UsageCallSession(statistics, start, Clock.systemUTC()); + when(statistics.describeCall(context, "decision.decide", false, metadata)) + .thenReturn(descriptor); + when(statistics.beginCall(descriptor)).thenReturn(session); + var response = new DecisionResponse("jev-actual", Map.of("spam", + DecisionAnswer.builder().type(DecisionQuestionType.YES_NO).probability(0.2).build()), + hasUsage ? new DecisionUsage(12, 3) : null); + var runtime = new DecisionModelImpl(ignored -> Mono.just(response), + new UsageExecutionObserver(), new TypeSafeProvider().decisionCapabilities()); + var model = new AuditedDecisionModel(runtime, context, audit, statistics); + + assertThat(model.decide(DecisionRequest.builder().state("comment") + .questions(Map.of("spam", DecisionQuestion.yesNo("Is it spam?"))) + .metadata(metadata).build()).block()).isSameAs(response); + + verify(audit).recordModelInvocation(context, "decision.decide"); + var record = ArgumentCaptor.forClass(UsageExecutionRecord.class); + verify(statistics).recordExecution(eq(session), record.capture()); + assertThat(record.getValue().unitKind()).isEqualTo(UsageUnitKind.DECISION); + assertThat(record.getValue().status()).isEqualTo(UsageStatus.SUCCEEDED); + assertThat(record.getValue().responseModelId()).isEqualTo("jev-actual"); + assertThat(record.getValue().usage().quality()).isEqualTo(hasUsage + ? UsageQuality.REPORTED_COMPONENTS : UsageQuality.MISSING); + assertThat(record.getValue().usage().accountedTotalTokens()) + .isEqualTo(hasUsage ? 15L : null); + verify(statistics).finishCall(eq(session), any()); + } +} diff --git a/app/src/test/java/run/halo/aifoundation/service/decision/DecisionModelTest.java b/app/src/test/java/run/halo/aifoundation/service/decision/DecisionModelTest.java new file mode 100644 index 00000000..1500907d --- /dev/null +++ b/app/src/test/java/run/halo/aifoundation/service/decision/DecisionModelTest.java @@ -0,0 +1,98 @@ +package run.halo.aifoundation.service.decision; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import run.halo.aifoundation.chat.GenerationTimeouts; +import run.halo.aifoundation.decision.DecisionAnswer; +import run.halo.aifoundation.decision.DecisionQuestion; +import run.halo.aifoundation.decision.DecisionQuestionType; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.decision.DecisionResponse; +import run.halo.aifoundation.exception.DecisionCancelledException; +import run.halo.aifoundation.exception.DecisionTimeoutException; +import run.halo.aifoundation.provider.support.ProviderDecisionClient; +import run.halo.aifoundation.provider.typesafe.TypeSafeProvider; +import run.halo.aifoundation.service.observation.UsageExecutionObserver; + +class DecisionModelTest { + private final ProviderDecisionClient client = mock(ProviderDecisionClient.class); + private final DecisionModelImpl model = new DecisionModelImpl(client, + new UsageExecutionObserver(), new TypeSafeProvider().decisionCapabilities()); + + @Test + void validatesBeforeCallingProvider() { + var badType = DecisionQuestion.choice("Route?", Map.of("only", "only")); + var tooManyLevels = DecisionQuestion.score("Score?", java.util.Collections.nCopies(11, "level")); + for (var request : List.of( + DecisionRequest.builder().state(" ").questions(Map.of("q", DecisionQuestion.yesNo("?"))).build(), + DecisionRequest.builder().state("text").questions(Map.of()).build(), + DecisionRequest.builder().state("text").questions(Map.of("q", badType)).build(), + DecisionRequest.builder().state("text").questions(Map.of("q", tooManyLevels)).build(), + DecisionRequest.builder().state(new Object()).questions(Map.of("q", DecisionQuestion.yesNo("?"))).build())) { + StepVerifier.create(model.decide(request)).expectError(IllegalArgumentException.class).verify(); + } + verifyNoInteractions(client); + } + + @Test + void rejectsMismatchedQuestionIdsTypesAndInvalidProbabilities() { + var valid = DecisionAnswer.builder().type(DecisionQuestionType.YES_NO).probability(0.5).build(); + for (var answer : List.of( + Map.of("wrong", valid), + Map.of("q", DecisionAnswer.builder().type(DecisionQuestionType.CHOICE).choice("a").build()), + Map.of("q", DecisionAnswer.builder().type(DecisionQuestionType.YES_NO).probability(1.1).build()), + Map.of("q", DecisionAnswer.builder().type(DecisionQuestionType.YES_NO).probability(Double.NaN).build()))) { + when(client.decide(any())).thenReturn(Mono.just(new DecisionResponse("jev", answer, null))); + StepVerifier.create(model.decide(request())).expectError(IllegalStateException.class).verify(); + } + } + + @Test + void rejectsInvalidDistributionAndUnknownChoice() { + var request = request(); + request.setQuestions(Map.of("q", DecisionQuestion.choice("Choose", Map.of("a", "A", "b", "B")))); + for (var answer : List.of( + DecisionAnswer.builder().type(DecisionQuestionType.CHOICE).choice("a") + .probabilities(Map.of("a", 0.1, "b", 0.1)).confidence(0.2).build(), + DecisionAnswer.builder().type(DecisionQuestionType.CHOICE).choice("unknown") + .probabilities(Map.of("a", 0.8, "b", 0.2)).confidence(0.6).build())) { + when(client.decide(any())).thenReturn(Mono.just(new DecisionResponse("jev", Map.of("q", answer), null))); + StepVerifier.create(model.decide(request)).expectError(IllegalStateException.class).verify(); + } + } + + @Test + void honoursCancellationAndTimeout() { + var cancelled = request(); + cancelled.setCancellationToken(() -> true); + StepVerifier.create(model.decide(cancelled)).expectError(DecisionCancelledException.class).verify(); + verifyNoInteractions(client); + when(client.decide(any())).thenReturn(Mono.never()); + var timed = request(); + timed.setTimeouts(GenerationTimeouts.builder().totalTimeout(Duration.ofSeconds(1)).build()); + StepVerifier.withVirtualTime(() -> model.decide(timed)).thenAwait(Duration.ofSeconds(1)) + .expectError(DecisionTimeoutException.class).verify(); + } + + @Test + void capabilitySnapshotsCannotMutateRuntimeLimits() { + var capability = model.capabilities(); + capability.setMaxScoreLevels(100); + assertThat(model.capabilities().getMaxScoreLevels()).isEqualTo(10); + } + + private static DecisionRequest request() { + return DecisionRequest.builder().state("message") + .questions(Map.of("q", DecisionQuestion.yesNo("Is it urgent?"))).build(); + } +} diff --git a/dev/en/README.md b/dev/en/README.md index 9f4ea3f5..0f4deba2 100644 --- a/dev/en/README.md +++ b/dev/en/README.md @@ -27,7 +27,7 @@ selector using the repository's current public contracts. ## SDK surfaces -[SDK Core](./sdk-core/README.md) resolves language, embedding, reranking, and image models and +[SDK Core](./sdk-core/README.md) resolves language, embedding, reranking, image, and decision models and provides text generation, multimodal input, structured output, tools, RAG, middleware, cancellation, lifecycle, and the Java UI Message bridge. diff --git a/dev/en/model-selector.md b/dev/en/model-selector.md index e7943ce7..582078e2 100644 --- a/dev/en/model-selector.md +++ b/dev/en/model-selector.md @@ -15,7 +15,7 @@ AI Foundation registers the FormKit input type `aiModelSelector`. Its saved valu validation: required ``` -`modelType` accepts `language`, `embedding`, `rerank`, or `image-generation`. +`modelType` accepts `language`, `embedding`, `rerank`, `image-generation`, or `decision`. ## Capability requirements diff --git a/dev/en/sdk-core/README.md b/dev/en/sdk-core/README.md index 0e9184d8..a00086cc 100644 --- a/dev/en/sdk-core/README.md +++ b/dev/en/sdk-core/README.md @@ -18,7 +18,8 @@ parameter mappings. 6. [Image generation](./image-generation.md) 7. [Middleware, step control, and lifecycle](./middleware-and-lifecycle.md) 8. [Error handling](./error-handling.md) -9. [Complete public API index](./api-reference.md) +9. [Decision models](./decision-models.md) +10. [Complete public API index](./api-reference.md) ## Entry points diff --git a/dev/en/sdk-core/api-reference.md b/dev/en/sdk-core/api-reference.md index 679db169..8a990347 100644 --- a/dev/en/sdk-core/api-reference.md +++ b/dev/en/sdk-core/api-reference.md @@ -11,7 +11,7 @@ not consumer APIs. | Area | Public types | Purpose | | ----------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| Service | `AiModelService` | Resolve default or named language, embedding, reranking, and image models. | +| Service | `AiModelService` | Resolve default or named language, embedding, reranking, image, and decision models. | | Model identity | `ModelInfo`, `ProviderInfo` | Read-only resolved model and provider information. | | Capability snapshot | `ModelCapabilities`, `LanguageCapability`, `ImageGenerationCapability` | Provider-neutral effective capabilities. | | Capability requirements | `ModelCapabilityRequirement`, `InputSource` | Positive all-of requirements for model selection. | @@ -127,6 +127,17 @@ The main `ToolDefinition` builder fields are `name`, `description`, `inputSchema | Policies | `RagPromptPlacement`, `RagFailurePolicy`, `RagEmptyContextPolicy` | | Observation | `RagLifecycle`, `RagLifecycleEvent` | +## Decision models + +Resolve with `AiModelService.decisionModel()` or `decisionModel(modelName)`. See the [decision model guide](./decision-models.md). + +| Area | Public types | +| --- | --- | +| Inference | `DecisionModel`, `DecisionRequest`, `DecisionResponse` | +| Typed questions and results | `DecisionQuestion`, `DecisionQuestionType`, `DecisionAnswer` | +| Capabilities and token usage | `DecisionCapability`, `DecisionUsage` | +| Request control errors | `DecisionCancelledException`, `DecisionTimeoutException` | + ## Image generation | Area | Public types | diff --git a/dev/en/sdk-core/decision-models.md b/dev/en/sdk-core/decision-models.md new file mode 100644 index 00000000..61ff80ad --- /dev/null +++ b/dev/en/sdk-core/decision-models.md @@ -0,0 +1,83 @@ +# Decision models + +[简体中文](../../zh-CN/sdk-core/decision-models.md) | English + +Decision models answer closed questions about shared text or JSON state. Consumer plugins own thresholds +and subsequent actions. These APIs are new in the development build and require matching plugin and +Java/npm SDK versions; the published `1.0.0` API does not include them. + +## Configure TypeSafe / Jev + +Create a **TypeSafe (Jev)** provider and select a Halo Secret for its API key. The default service root +is `https://api.typesafe.ai`; the adapter appends `/v1/systemone` and `/v1/models`. Import a discovered +model or manually enter an accepted provider model ID with model type `decision` and adapter +`typesafe-system-one`. Model discovery uses the account's catalog, not a hardcoded model list. +Optionally configure the default decision model, then use the Console's Decision workbench. + +The adapter supports text/JSON state and `YES_NO`, `CHOICE`, and `SCORE` questions. Choices support +2–255 options; scores support 2–10 ordered levels. Images, Box questions, text generation, tool execution, +and automatic business routing are outside this adapter's current scope. Unsupported native options +are rejected explicitly. + +## Java + +```java +var request = DecisionRequest.builder() + .state(Map.of("comment", comment)) + .questions(Map.of( + "spam", DecisionQuestion.yesNo("Is this unsolicited advertising?"), + "intent", DecisionQuestion.choice("What is the intent?", + Map.of("question", "A question", "feedback", "Feedback", "other", "Other")), + "relevance", DecisionQuestion.score("How relevant is this to plugin setup?", + List.of("Unrelated", "Partially related", "Highly related")))) + .build(); + +return extensionGetter.getEnabledExtension(AiModelService.class) + .flatMap(service -> service.decisionModel(modelName)) + .flatMap(model -> model.decide(request)); +``` + +Decision types live in `run.halo.aifoundation.decision`. Resolve the service with Halo's +`ExtensionGetter`; `modelName` means `AiModel.metadata.name`. Null/blank or the no-argument overload +uses the default decision model. Request metadata stays in local audit records and is not sent upstream. + +`YES_NO.probability` is the probability of yes, not a boolean. `CHOICE` preserves its selected option, +distribution and confidence. `SCORE` preserves its fractional, zero-based level score, legend, +distribution and confidence. Confidence measures the distribution's concentration, not correctness; +YES_NO has no separate confidence. Unknown token counts remain null. The response reports the served model. +The runtime validates request fields, provider limits, response question IDs/types and distributions. + +`model.capabilities()` returns supported question types and limits. Selectors can filter by +`modelType: decision` and `requiredCapabilities.decision`. A total timeout uses `GenerationTimeouts` +and raises `DecisionTimeoutException`. Subscription cancellation cancels HTTP work; cancellation tokens +are checked before and after inference and raise `DecisionCancelledException`. + +## Browser SDK + +Expose your own authenticated consumer-plugin endpoint accepting `DecisionRequest` and returning +`DecisionResponse`. Provider keys stay server-side. + +```ts +import { createDecisionClient } from '@halo-dev/ai-foundation-sdk' + +const client = createDecisionClient({ + api: '/apis/your-plugin.example/v1alpha1/decide', // Implement this endpoint in your plugin. + credentials: 'same-origin', +}) +const result = await client.decide({ + state: { comment: 'Thanks for the article!' }, + questions: { spam: { type: 'YES_NO', instructions: 'Is this spam?' } }, +}) +``` + +The client supports custom fetch, headers and AbortSignal and validates typed response probabilities. +It targets your endpoint, not the administrator-only Console test API. That test API accepts a string +`state` plus `stateFormat: TEXT | JSON`; the server parses JSON before inference. + +Existing APIs and persisted values are preserved, and new AiModelService methods have compatible defaults. +Older releases/clients cannot recognize the new model and adapter enum values: update consuming SDKs, +and remove decision resources/default slots before downgrading. Automated tests cover the protocol, +runtime and workbench; evaluate Chinese/domain-specific accuracy and thresholds with your own data. + +References: [System One](https://docs.typesafe.ai/api), [model discovery](https://docs.typesafe.ai/api/list-models), +[confidence](https://docs.typesafe.ai/confidence). diff --git a/dev/zh-CN/dev.md b/dev/zh-CN/dev.md index 57dbde52..96787385 100644 --- a/dev/zh-CN/dev.md +++ b/dev/zh-CN/dev.md @@ -1037,7 +1037,7 @@ return model.generateText(request) `aiModelSelector` 是本插件提供的 FormKit 输入类型,可在调用方插件设置页中让用户选择模型。 保存值是 `AiModel.metadata.name`,可直接传给 `languageModel(modelName)`、`embeddingModel(modelName)`、 -`rerankingModel(modelName)` 或 `imageGenerationModel(modelName)`。 +`rerankingModel(modelName)`、`imageGenerationModel(modelName)` 或 `decisionModel(modelName)`。 FormKit Schema 示例: @@ -1082,7 +1082,7 @@ formSchema: | Prop | 说明 | | ---------------------- | ------------------------------------------------------------ | -| `modelType` | 筛选 `language`、`embedding`、`rerank` 或 `image-generation` | +| `modelType` | 筛选 `language`、`embedding`、`rerank`、`image-generation` 或 `decision` | | `providerName` | 筛选指定 `AiProvider.metadata.name` | | `providerType` | 筛选指定供应方类型 | | `enabled` | 按 `AiModel.spec.enabled` 筛选 | diff --git a/dev/zh-CN/model-selector.md b/dev/zh-CN/model-selector.md index fdd02ad1..36b7c493 100644 --- a/dev/zh-CN/model-selector.md +++ b/dev/zh-CN/model-selector.md @@ -41,7 +41,7 @@ formSchema: modelType: image-generation ``` -`modelType` 支持 `language`、`embedding`、`rerank` 和 `image-generation`。 +`modelType` 支持 `language`、`embedding`、`rerank`、`image-generation` 和 `decision`。 ## 按常用能力提醒 diff --git a/dev/zh-CN/sdk-core/README.md b/dev/zh-CN/sdk-core/README.md index 24a87e95..c229f41b 100644 --- a/dev/zh-CN/sdk-core/README.md +++ b/dev/zh-CN/sdk-core/README.md @@ -28,7 +28,8 @@ ExtensionGetter 6. [图像生成](./image-generation.md) 7. [Middleware、步骤控制与生命周期](./middleware-and-lifecycle.md) 8. [错误处理](./error-handling.md) -9. [完整公开 API 索引](./api-reference.md) +9. [决策模型](./decision-models.md) +10. [完整公开 API 索引](./api-reference.md) ## API 入口速览 diff --git a/dev/zh-CN/sdk-core/api-reference.md b/dev/zh-CN/sdk-core/api-reference.md index cf9f52a9..40e6f733 100644 --- a/dev/zh-CN/sdk-core/api-reference.md +++ b/dev/zh-CN/sdk-core/api-reference.md @@ -13,12 +13,12 @@ | 类型 | 用途 | | ---------------- | ----------------------------------------------------------------- | -| `AiModelService` | 跨插件解析默认或指定名称的语言、Embedding、Rerank、图像生成模型。 | +| `AiModelService` | 跨插件解析默认或指定名称的语言、Embedding、Rerank、图像生成和决策模型。 | | `ModelInfo` | 已解析模型的只读身份和模型级信息。 | | `ProviderInfo` | 已解析模型所属 Provider 的只读信息。 | -`AiModelService` 的八个解析方法分为四组:`languageModel`、`embeddingModel`、 -`rerankingModel`、`imageGenerationModel`,每组都提供无参默认模型和接收 +`AiModelService` 的十个解析方法分为五组:`languageModel`、`embeddingModel`、 +`rerankingModel`、`imageGenerationModel`、`decisionModel`,每组都提供无参默认模型和接收 `AiModel.metadata.name` 的命名重载。 ## Capability @@ -222,6 +222,17 @@ 完整组合示例见 [Embedding、Rerank 与 RAG](./embeddings-reranking-and-rag.md)。 +## 决策模型 + +通过 `AiModelService.decisionModel()` 或 `decisionModel(modelName)` 获取,详见[决策模型指南](./decision-models.md)。 + +| 类型 | 用途 | +| --- | --- | +| `DecisionModel`、`DecisionRequest`、`DecisionResponse` | 文本或 JSON 状态的批量封闭问题判断。 | +| `DecisionQuestion`、`DecisionQuestionType`、`DecisionAnswer` | 概率判断、单选和有序评分。 | +| `DecisionCapability`、`DecisionUsage` | 问题类型、输入与数量限制以及可空 token 用量。 | +| `DecisionCancelledException`、`DecisionTimeoutException` | 取消与总调用超时。 | + ## 图像生成 | 类型 | 用途 | diff --git a/dev/zh-CN/sdk-core/decision-models.md b/dev/zh-CN/sdk-core/decision-models.md new file mode 100644 index 00000000..36a171a2 --- /dev/null +++ b/dev/zh-CN/sdk-core/decision-models.md @@ -0,0 +1,97 @@ +# 决策模型 + +简体中文 | [English](../../en/sdk-core/decision-models.md) + +决策模型针对共享内容回答封闭问题,返回概率判断、候选选择或有序评分。业务插件决定阈值和后续动作。 +这些 API 是本次开发新增能力,使用时需要包含此功能的 AI Foundation 插件和 Java/npm SDK;已发布的 +`1.0.0` API 不包含它们。 + +## 配置 TypeSafe / Jev + +1. 在提供商管理中选择 **TypeSafe (Jev)**,通过 Halo Secret 配置 API Key。 +2. 默认 Base URL 为 `https://api.typesafe.ai`(服务根地址)。可配置代理;路径由适配器追加。 +3. 从提供商发现并导入模型,或手动填写供应方允许的模型 ID。模型类型选择 `decision`,适配器为 + `typesafe-system-one`。模型列表来自 `GET /v1/models`,没有硬编码版本列表。 +4. 可在默认模型页设置默认决策模型,在工作台的“决策”页输入文本或 JSON,并添加多个问题。 + +当前适配器支持文本与 JSON 状态,以及 `YES_NO`、`CHOICE`、`SCORE`。每个单选问题最多 255 个选项, +评分问题为 2–10 个有序等级。不提供图片/Box、文本生成、工具执行或自动业务路由。 +原生参数不支持时会明确拒绝,不会静默忽略。 + +## Java 调用 + +```java +import java.util.List; +import java.util.Map; +import run.halo.aifoundation.AiModelService; +import run.halo.aifoundation.decision.DecisionQuestion; +import run.halo.aifoundation.decision.DecisionRequest; + +var request = DecisionRequest.builder() + .state(Map.of("comment", "这篇文章很有帮助,请问如何配置插件?")) + .questions(Map.of( + "spam", DecisionQuestion.yesNo("这条评论是否为垃圾广告?"), + "intent", DecisionQuestion.choice("评论的主要意图是什么?", Map.of( + "question", "提问", "feedback", "反馈", "other", "其他")), + "relevance", DecisionQuestion.score("与插件配置主题有多相关?", + List.of("无关", "部分相关", "高度相关")))) + .metadata(Map.of("feature", "comment-review")) + .build(); + +return extensionGetter.getEnabledExtension(AiModelService.class) + .flatMap(service -> service.decisionModel(modelName)) + .flatMap(model -> model.decide(request)); +``` + +`modelName` 为 `AiModel.metadata.name`;无参或空白名称使用默认决策模型。跨插件仍通过 +`ExtensionGetter` 获取服务。`metadata` 仅供本地审计,不会发送给提供商。 + +- `YES_NO` 的 `probability` 是“是”的概率,不会自动转为布尔值。TypeSafe 的 `noul` 在适配器内部转换。 +- `CHOICE` 保留 `choice`、完整 `probabilities` 和 `confidence`。 +- `SCORE` 保留从 0 开始的浮点评分、等级 `legend`、概率分布和置信度。 +- `confidence` 描述分布的集中程度,不等于答案正确率;`YES_NO` 没有单独的置信度字段。 +- `response.model` 保留实际服务模型,token 数量未知时保持 `null`,不能按零用量处理。 +- 请求验证先于推理;返回结果的问题 ID、类型、选项、数值范围和概率分布也会校验。 + +`DecisionRequest.timeouts` 接受 `GenerationTimeouts.totalTimeout`,超时抛出 +`DecisionTimeoutException`。取消 Reactor 订阅会取消 HTTP 请求;`cancellationToken` 在调用前和返回时 +检查,抛出 `DecisionCancelledException`。通过 `model.capabilities()` 查看问题类型和数量限制。 +模型选择器可使用 `modelType: decision` 和 `requiredCapabilities.decision` 过滤。 + +## 浏览器调用自己的插件接口 + +消费插件可以接收 `DecisionRequest`、调用 `AiModelService`,并把 `DecisionResponse` 作为 JSON 返回。 +使用消费插件自己的鉴权和业务校验;浏览器不直接持有 TypeSafe API Key。 + +```ts +import { createDecisionClient } from '@halo-dev/ai-foundation-sdk' + +const client = createDecisionClient({ + api: '/apis/your-plugin.example/v1alpha1/decide', // 由消费插件实现 + credentials: 'same-origin', +}) +const result = await client.decide({ + state: { comment: '这篇文章很有帮助' }, + questions: { spam: { type: 'YES_NO', instructions: '是否为垃圾广告?' } }, +}) +const answer = result.answers.spam +if (answer?.type === 'YES_NO') { + console.log(answer.probability) +} +``` + +客户端支持自定义 `fetch`、请求头和 `AbortSignal`,并检查响应的决策类型及概率。它调用上述消费插件 +接口,不调用仅限管理员的 Console 测试端点。Console 测试端点为便于表单与 OpenAPI 客户端使用,接收 +字符串 `state` 和可选的 `stateFormat: TEXT | JSON`;JSON 格式在服务端解析后才交给模型。 + +## 兼容性与验证 + +原有语言、嵌入、重排和图像 API 及持久化值保持不变。新服务方法提供默认实现,旧的 +`AiModelService` 实现可继续加载;新功能仍要求匹配版本的运行时。 +新增 `decision` 模型类型、`typesafe-system-one` 适配器及默认决策槽位后,旧版插件/生成客户端不能 +识别新枚举。升级时一起更新相关 SDK;降级前移除新模型资源并清空决策默认槽位。 + +自动测试覆盖协议、模型解析、验证、取消、超时和工作台。实际中文业务效果与阈值应使用自己的数据评估。 + +协议依据:[System One](https://docs.typesafe.ai/api)、[模型发现](https://docs.typesafe.ai/api/list-models)、 +[概率与置信度](https://docs.typesafe.ai/confidence)。 diff --git a/ui/packages/sdk/README.md b/ui/packages/sdk/README.md index c7ea9e13..e0223176 100644 --- a/ui/packages/sdk/README.md +++ b/ui/packages/sdk/README.md @@ -437,3 +437,10 @@ pnpm --dir ui --filter @halo-dev/ai-foundation-sdk typecheck pnpm --dir ui --filter @halo-dev/ai-foundation-sdk test pnpm --dir ui --filter @halo-dev/ai-foundation-sdk build ``` + +## Decision models + +Use `createDecisionClient({ api })` with your consumer plugin's decision endpoint for typed probability, +choice and score results. Custom fetch, headers and AbortSignal are supported; provider keys stay on the +server. This feature requires plugin/SDK builds that include decision support. See the +[decision model guide](../../../dev/en/sdk-core/decision-models.md) for setup and Java/browser examples. diff --git a/ui/packages/sdk/README.zh-CN.md b/ui/packages/sdk/README.zh-CN.md index ca4bcec6..b1f2b82a 100644 --- a/ui/packages/sdk/README.zh-CN.md +++ b/ui/packages/sdk/README.zh-CN.md @@ -411,3 +411,9 @@ pnpm --dir ui --filter @halo-dev/ai-foundation-sdk typecheck pnpm --dir ui --filter @halo-dev/ai-foundation-sdk test pnpm --dir ui --filter @halo-dev/ai-foundation-sdk build ``` + +## 决策模型 + +通过 `createDecisionClient({ api })` 调用消费插件自己的决策端点,返回类型化的概率、单选或评分。 +支持请求头、自定义 fetch 和 AbortSignal;供应商 API Key 留在后端。此能力需要包含决策模型支持的新版本插件与 SDK。 +配置方式、Java 后端和浏览器示例见[决策模型指南](../../../dev/zh-CN/sdk-core/decision-models.md)。 diff --git a/ui/packages/sdk/src/decision.test.ts b/ui/packages/sdk/src/decision.test.ts new file mode 100644 index 00000000..f9ed4742 --- /dev/null +++ b/ui/packages/sdk/src/decision.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it, vi } from 'vitest' +import { createDecisionClient, type DecisionRequest } from './decision' +import { AIUIError, AIUIProtocolError } from './errors' + +const request: DecisionRequest = { + state: { comment: 'hello' }, + questions: { spam: { type: 'YES_NO', instructions: 'Spam?' } }, +} +const result = { + model: 'jev-version', + answers: { spam: { type: 'YES_NO', probability: 0.12 } }, + usage: { inputTokens: 10, outputTokens: null }, +} + +describe('decision client', () => { + it('posts to the consumer endpoint and preserves probability and unknown token counts', async () => { + const fetch = vi.fn().mockResolvedValue(Response.json(result)) + const controller = new AbortController() + const client = createDecisionClient({ + api: '/apis/my-plugin/decide', + fetch, + headers: { 'X-Request': 'example' }, + credentials: 'same-origin', + }) + expect(await client.decide(request, { signal: controller.signal })).toEqual(result) + expect(fetch).toHaveBeenCalledWith( + '/apis/my-plugin/decide', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(request), + signal: controller.signal, + credentials: 'same-origin', + }), + ) + expect(fetch.mock.calls[0]?.[1].headers.get('X-Request')).toBe('example') + }) + + it.each([ + { model: 'jev', answers: {} }, + { model: 'jev', answers: { spam: { type: 'YES_NO', probability: 2 } } }, + { model: 'jev', answers: { spam: { type: 'CHOICE', choice: 'no' } } }, + { ...result, usage: { inputTokens: -1 } }, + ])('rejects invalid decision payloads', async (payload) => { + const client = createDecisionClient({ + api: '/decision', + fetch: vi.fn().mockResolvedValue(Response.json(payload)), + }) + await expect(client.decide(request)).rejects.toBeInstanceOf(AIUIProtocolError) + }) + + it('reports HTTP errors with status', async () => { + const client = createDecisionClient({ + api: '/decision', + fetch: vi.fn().mockResolvedValue(new Response('', { status: 429 })), + }) + await expect(client.decide(request)).rejects.toMatchObject>({ status: 429 }) + }) + + it('preserves choice distributions and fractional scores, rejecting incomplete distributions', async () => { + const mixed: DecisionRequest = { + state: ['comment'], + questions: { + intent: { type: 'CHOICE', instructions: 'Intent?', options: { a: 'A', b: 'B' } }, + rating: { type: 'SCORE', instructions: 'Rating?', levels: ['low', 'high'] }, + }, + } + const payload = { + model: 'jev-version', + answers: { + intent: { type: 'CHOICE', choice: 'a', confidence: 0.6, probabilities: { a: 0.8, b: 0.2 } }, + rating: { + type: 'SCORE', + score: 0.8, + confidence: 0.6, + probabilities: { '0': 0.2, '1': 0.8 }, + legend: { '0': 'low', '1': 'high' }, + }, + }, + } + const fetch = vi.fn().mockResolvedValueOnce(Response.json(payload)) + const client = createDecisionClient({ api: '/decision', fetch }) + expect(await client.decide(mixed)).toEqual(payload) + fetch.mockResolvedValueOnce( + Response.json({ + ...payload, + answers: { + ...payload.answers, + intent: { ...payload.answers.intent, probabilities: { a: 1 } }, + }, + }), + ) + await expect(client.decide(mixed)).rejects.toBeInstanceOf(AIUIProtocolError) + }) +}) diff --git a/ui/packages/sdk/src/decision.ts b/ui/packages/sdk/src/decision.ts new file mode 100644 index 00000000..8854ace5 --- /dev/null +++ b/ui/packages/sdk/src/decision.ts @@ -0,0 +1,166 @@ +import { AIUIError, AIUIProtocolError } from './errors' +import type { FetchFunction } from './types' + +export type DecisionJsonValue = + | string + | number + | boolean + | null + | DecisionJsonValue[] + | { [key: string]: DecisionJsonValue } +export type DecisionState = string | DecisionJsonValue[] | { [key: string]: DecisionJsonValue } + +export type DecisionQuestion = + | { type: 'YES_NO'; instructions: string; yesCriteria?: string; noCriteria?: string } + | { type: 'CHOICE'; instructions: string; options: Record } + | { type: 'SCORE'; instructions: string; levels: string[] } + +export type DecisionAnswer = + | { type: 'YES_NO'; probability: number } + | { type: 'CHOICE'; choice: string; probabilities: Record; confidence: number } + | { + type: 'SCORE' + score: number + probabilities: Record + confidence: number + legend: Record + } + +export interface DecisionRequest { + state: DecisionState + questions: Record +} + +export interface DecisionResponse { + model: string + answers: Record + usage?: { inputTokens?: number | null; outputTokens?: number | null } | null +} + +export interface DecisionClientOptions { + /** A consumer-plugin endpoint that returns AI Foundation's DecisionResponse. */ + api: string + headers?: HeadersInit + credentials?: RequestCredentials + fetch?: FetchFunction +} + +/** Calls your plugin's decision endpoint. Provider API keys remain on the server. */ +export function createDecisionClient(options: DecisionClientOptions) { + return { + async decide( + request: DecisionRequest, + callOptions: { signal?: AbortSignal } = {}, + ): Promise { + const headers = new Headers(options.headers) + headers.set('Content-Type', 'application/json') + const response = await (options.fetch ?? globalThis.fetch)(options.api, { + method: 'POST', + headers, + credentials: options.credentials, + signal: callOptions.signal, + body: JSON.stringify(request), + }) + if (!response.ok) { + throw new AIUIError(`Decision request failed (${response.status})`, { + status: response.status, + response, + }) + } + let result: unknown + try { + result = await response.json() + } catch (cause) { + throw new AIUIProtocolError('Decision endpoint returned invalid JSON', { cause }) + } + validateDecisionResponse(result, request) + return result + }, + } +} + +function object(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new AIUIProtocolError(message) +} + +function probability(value: unknown): value is number { + return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1 +} + +function sameKeys(left: Record, keys: string[]) { + return ( + Object.keys(left).length === keys.length && + keys.every((key) => Object.prototype.hasOwnProperty.call(left, key)) + ) +} + +function validateDecisionResponse( + value: unknown, + request: DecisionRequest, +): asserts value is DecisionResponse { + assert( + object(value) && typeof value.model === 'string' && value.model.trim(), + 'Missing decision response model', + ) + assert( + object(value.answers) && sameKeys(value.answers, Object.keys(request.questions)), + 'Decision question IDs do not match', + ) + for (const [id, question] of Object.entries(request.questions)) { + const answer = value.answers[id] + assert(object(answer) && answer.type === question.type, `Invalid answer type for ${id}`) + if (question.type === 'YES_NO') { + assert(probability(answer.probability), `Invalid yes/no probability for ${id}`) + continue + } + const keys = + question.type === 'CHOICE' + ? Object.keys(question.options) + : question.levels.map((_, index) => String(index)) + assert( + object(answer.probabilities) && sameKeys(answer.probabilities, keys), + `Invalid distribution for ${id}`, + ) + const values = Object.values(answer.probabilities) + assert(values.every(probability), `Invalid probabilities for ${id}`) + assert( + Math.abs(values.reduce((sum, item) => sum + item, 0) - 1) <= 0.001, + `Probabilities must sum to one for ${id}`, + ) + assert(probability(answer.confidence), `Invalid confidence for ${id}`) + if (question.type === 'CHOICE') { + assert( + typeof answer.choice === 'string' && keys.includes(answer.choice), + `Unknown choice for ${id}`, + ) + } else { + assert( + typeof answer.score === 'number' && + Number.isFinite(answer.score) && + answer.score >= 0 && + answer.score <= question.levels.length - 1, + `Invalid score for ${id}`, + ) + assert( + object(answer.legend) && + sameKeys(answer.legend, keys) && + Object.values(answer.legend).every((item) => typeof item === 'string'), + `Invalid score legend for ${id}`, + ) + } + } + if (value.usage != null) { + assert(object(value.usage), 'Invalid decision token usage') + for (const key of ['inputTokens', 'outputTokens']) { + const count = value.usage[key] + assert( + count == null || (typeof count === 'number' && Number.isSafeInteger(count) && count >= 0), + 'Invalid decision token count', + ) + } + } +} diff --git a/ui/packages/sdk/src/index.ts b/ui/packages/sdk/src/index.ts index 9841ab07..09b7c790 100644 --- a/ui/packages/sdk/src/index.ts +++ b/ui/packages/sdk/src/index.ts @@ -6,6 +6,16 @@ export { lastAssistantMessageIsCompleteWithToolCalls, } from './chat' export type { ChatInit, ChatStateAdapter, SendMessageInput } from './chat' +export { createDecisionClient } from './decision' +export type { + DecisionAnswer, + DecisionClientOptions, + DecisionJsonValue, + DecisionQuestion, + DecisionRequest, + DecisionResponse, + DecisionState, +} from './decision' export { AIUIError, AIUIProtocolError, AIUISchemaValidationError, isProtocolError } from './errors' export type { AIUISchemaValidationErrorOptions, AIUISchemaValidationTarget } from './errors' export { filePartFromFile, filePartsFromFiles } from './files' @@ -15,6 +25,7 @@ export { applyUIMessageChunk, createUIMessageReducer, messageText } from './mess export type { CreateReducerOptions, UIMessageReducerState } from './message-reducer' export { fromOpenAPIRequestArgs } from './openapi' export type { OpenAPIRequestArgs } from './openapi' +export { fixJson } from './partial-json' export { AIUIMessageValidationError, assertValidUIMessages, @@ -27,7 +38,6 @@ export type { ValidateUIMessagesOptions, } from './persistence' export { parsePartialJson, toJsonSchema, validateFinalValue, validateRuntimeSchema } from './schema' -export { fixJson } from './partial-json' export type { DataPartSchemas, MessageMetadataSchema, diff --git a/ui/src/api/generated/.openapi-generator/FILES b/ui/src/api/generated/.openapi-generator/FILES index 4e1e0d5c..e61bdc34 100644 --- a/ui/src/api/generated/.openapi-generator/FILES +++ b/ui/src/api/generated/.openapi-generator/FILES @@ -28,6 +28,11 @@ models/ai-provider.ts models/caller-plugin-info.ts models/copy-operation.ts models/data-content.ts +models/decision-answer.ts +models/decision-capability.ts +models/decision-question.ts +models/decision-response.ts +models/decision-usage.ts models/default-model-slots.ts models/default-parameter-mapping-info.ts models/discovered-model-item.ts @@ -74,6 +79,7 @@ models/rerank-warning.ts models/reset-request.ts models/selection.ts models/test-completion-stream-request.ts +models/test-decision-request.ts models/test-embedding-request.ts models/test-embedding-response.ts models/test-generated-file.ts diff --git a/ui/src/api/generated/api.ts b/ui/src/api/generated/api.ts index 3654e1fa..ee18b40d 100644 --- a/ui/src/api/generated/api.ts +++ b/ui/src/api/generated/api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/ai-model-v1alpha1-api.ts b/ui/src/api/generated/api/ai-model-v1alpha1-api.ts index b5989fee..e86d4011 100644 --- a/ui/src/api/generated/api/ai-model-v1alpha1-api.ts +++ b/ui/src/api/generated/api/ai-model-v1alpha1-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/ai-provider-v1alpha1-api.ts b/ui/src/api/generated/api/ai-provider-v1alpha1-api.ts index e92a112b..3a99af89 100644 --- a/ui/src/api/generated/api/ai-provider-v1alpha1-api.ts +++ b/ui/src/api/generated/api/ai-provider-v1alpha1-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-caller-plugin-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-caller-plugin-api.ts index 572931e5..189626fd 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-caller-plugin-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-caller-plugin-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-default-model-slot-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-default-model-slot-api.ts index 34ea5e58..c159ecee 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-default-model-slot-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-default-model-slot-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-api.ts index 7016ee4f..9b65d9e5 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -24,8 +24,12 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError // @ts-ignore import type { AiModel } from '../models'; // @ts-ignore +import type { DecisionResponse } from '../models'; +// @ts-ignore import type { TestCompletionStreamRequest } from '../models'; // @ts-ignore +import type { TestDecisionRequest } from '../models'; +// @ts-ignore import type { TestEmbeddingRequest } from '../models'; // @ts-ignore import type { TestEmbeddingResponse } from '../models'; @@ -229,6 +233,53 @@ export const ConsoleApiAifoundationHaloRunV1alpha1ModelApiAxiosParamCreator = fu options: localVarRequestOptions, }; }, + /** + * Evaluate typed questions against a shared text or JSON state. + * @param {string} name Model name (AiModel.metadata.name) + * @param {TestDecisionRequest} testDecisionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + testModelDecision: async (name: string, testDecisionRequest: TestDecisionRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'name' is not null or undefined + assertParamExists('testModelDecision', 'name', name) + // verify required parameter 'testDecisionRequest' is not null or undefined + assertParamExists('testModelDecision', 'testDecisionRequest', testDecisionRequest) + const localVarPath = `/apis/console.api.aifoundation.halo.run/v1alpha1/models/{name}/test-decision` + .replace(`{${"name"}}`, encodeURIComponent(String(name))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication basicAuth required + // http basic authentication required + setBasicAuthToObject(localVarRequestOptions, configuration) + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(testDecisionRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Test embedding generation with embedding settings and diagnostics. * @param {string} name Model name (AiModel.metadata.name) @@ -643,6 +694,19 @@ export const ConsoleApiAifoundationHaloRunV1alpha1ModelApiFp = function(configur const localVarOperationServerBasePath = operationServerMap['ConsoleApiAifoundationHaloRunV1alpha1ModelApi.testModelCompletionStream']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Evaluate typed questions against a shared text or JSON state. + * @param {string} name Model name (AiModel.metadata.name) + * @param {TestDecisionRequest} testDecisionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async testModelDecision(name: string, testDecisionRequest: TestDecisionRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.testModelDecision(name, testDecisionRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ConsoleApiAifoundationHaloRunV1alpha1ModelApi.testModelDecision']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Test embedding generation with embedding settings and diagnostics. * @param {string} name Model name (AiModel.metadata.name) @@ -785,6 +849,15 @@ export const ConsoleApiAifoundationHaloRunV1alpha1ModelApiFactory = function (co testModelCompletionStream(requestParameters: ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelCompletionStreamRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.testModelCompletionStream(requestParameters.name, requestParameters.testCompletionStreamRequest, options).then((request) => request(axios, basePath)); }, + /** + * Evaluate typed questions against a shared text or JSON state. + * @param {ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelDecisionRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + testModelDecision(requestParameters: ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelDecisionRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.testModelDecision(requestParameters.name, requestParameters.testDecisionRequest, options).then((request) => request(axios, basePath)); + }, /** * Test embedding generation with embedding settings and diagnostics. * @param {ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelEmbeddingRequest} requestParameters Request parameters. @@ -921,6 +994,27 @@ export interface ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelCompletio readonly testCompletionStreamRequest: TestCompletionStreamRequest } +/** + * Request parameters for testModelDecision operation in ConsoleApiAifoundationHaloRunV1alpha1ModelApi. + * @export + * @interface ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelDecisionRequest + */ +export interface ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelDecisionRequest { + /** + * Model name (AiModel.metadata.name) + * @type {string} + * @memberof ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelDecision + */ + readonly name: string + + /** + * + * @type {TestDecisionRequest} + * @memberof ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelDecision + */ + readonly testDecisionRequest: TestDecisionRequest +} + /** * Request parameters for testModelEmbedding operation in ConsoleApiAifoundationHaloRunV1alpha1ModelApi. * @export @@ -1154,6 +1248,17 @@ export class ConsoleApiAifoundationHaloRunV1alpha1ModelApi extends BaseAPI { return ConsoleApiAifoundationHaloRunV1alpha1ModelApiFp(this.configuration).testModelCompletionStream(requestParameters.name, requestParameters.testCompletionStreamRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * Evaluate typed questions against a shared text or JSON state. + * @param {ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelDecisionRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ConsoleApiAifoundationHaloRunV1alpha1ModelApi + */ + public testModelDecision(requestParameters: ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelDecisionRequest, options?: RawAxiosRequestConfig) { + return ConsoleApiAifoundationHaloRunV1alpha1ModelApiFp(this.configuration).testModelDecision(requestParameters.name, requestParameters.testDecisionRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * Test embedding generation with embedding settings and diagnostics. * @param {ConsoleApiAifoundationHaloRunV1alpha1ModelApiTestModelEmbeddingRequest} requestParameters Request parameters. diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-option-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-option-api.ts index 88f6425c..357616ac 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-option-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-option-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-api.ts index 1c8c45d2..958dbc45 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-type-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-type-api.ts index 83138ccb..eefc9fdd 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-type-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-type-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-usage-statistics-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-usage-statistics-api.ts index 83bea5b9..e44dd97d 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-usage-statistics-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-usage-statistics-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/base.ts b/ui/src/api/generated/base.ts index 8dc54c08..abe8c5ff 100644 --- a/ui/src/api/generated/base.ts +++ b/ui/src/api/generated/base.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/common.ts b/ui/src/api/generated/common.ts index d58f5967..c5d206d6 100644 --- a/ui/src/api/generated/common.ts +++ b/ui/src/api/generated/common.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/configuration.ts b/ui/src/api/generated/configuration.ts index 539aa90d..56b807da 100644 --- a/ui/src/api/generated/configuration.ts +++ b/ui/src/api/generated/configuration.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/index.ts b/ui/src/api/generated/index.ts index 8525548c..05d0cb9a 100644 --- a/ui/src/api/generated/index.ts +++ b/ui/src/api/generated/index.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/adapter-type-info.ts b/ui/src/api/generated/models/adapter-type-info.ts index 35bf307f..b975ad51 100644 --- a/ui/src/api/generated/models/adapter-type-info.ts +++ b/ui/src/api/generated/models/adapter-type-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -133,7 +133,8 @@ export const AdapterTypeInfoAdapterTypeEnum = { OllamaResponses: 'ollama-responses', OllamaMessages: 'ollama-messages', OllamaEmbedding: 'ollama-embedding', - OllamaImage: 'ollama-image' + OllamaImage: 'ollama-image', + TypesafeSystemOne: 'typesafe-system-one' } as const; export type AdapterTypeInfoAdapterTypeEnum = typeof AdapterTypeInfoAdapterTypeEnum[keyof typeof AdapterTypeInfoAdapterTypeEnum]; @@ -141,7 +142,8 @@ export const AdapterTypeInfoModelTypeEnum = { Language: 'language', Embedding: 'embedding', Rerank: 'rerank', - ImageGeneration: 'image-generation' + ImageGeneration: 'image-generation', + Decision: 'decision' } as const; export type AdapterTypeInfoModelTypeEnum = typeof AdapterTypeInfoModelTypeEnum[keyof typeof AdapterTypeInfoModelTypeEnum]; diff --git a/ui/src/api/generated/models/add-operation.ts b/ui/src/api/generated/models/add-operation.ts index 124f7bca..54382db1 100644 --- a/ui/src/api/generated/models/add-operation.ts +++ b/ui/src/api/generated/models/add-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-model-list.ts b/ui/src/api/generated/models/ai-model-list.ts index d2d0018c..c839c4b9 100644 --- a/ui/src/api/generated/models/ai-model-list.ts +++ b/ui/src/api/generated/models/ai-model-list.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-model-spec.ts b/ui/src/api/generated/models/ai-model-spec.ts index 37ec5830..115ed99f 100644 --- a/ui/src/api/generated/models/ai-model-spec.ts +++ b/ui/src/api/generated/models/ai-model-spec.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -175,7 +175,8 @@ export const AiModelSpecAdapterTypeEnum = { OllamaResponses: 'ollama-responses', OllamaMessages: 'ollama-messages', OllamaEmbedding: 'ollama-embedding', - OllamaImage: 'ollama-image' + OllamaImage: 'ollama-image', + TypesafeSystemOne: 'typesafe-system-one' } as const; export type AiModelSpecAdapterTypeEnum = typeof AiModelSpecAdapterTypeEnum[keyof typeof AiModelSpecAdapterTypeEnum]; @@ -208,7 +209,8 @@ export const AiModelSpecModelTypeEnum = { Language: 'language', Embedding: 'embedding', Rerank: 'rerank', - ImageGeneration: 'image-generation' + ImageGeneration: 'image-generation', + Decision: 'decision' } as const; export type AiModelSpecModelTypeEnum = typeof AiModelSpecModelTypeEnum[keyof typeof AiModelSpecModelTypeEnum]; diff --git a/ui/src/api/generated/models/ai-model.ts b/ui/src/api/generated/models/ai-model.ts index 6881905e..98499e5a 100644 --- a/ui/src/api/generated/models/ai-model.ts +++ b/ui/src/api/generated/models/ai-model.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-provider-list.ts b/ui/src/api/generated/models/ai-provider-list.ts index 1fc03045..bb4e6839 100644 --- a/ui/src/api/generated/models/ai-provider-list.ts +++ b/ui/src/api/generated/models/ai-provider-list.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-provider-spec.ts b/ui/src/api/generated/models/ai-provider-spec.ts index 65998812..0cfaae30 100644 --- a/ui/src/api/generated/models/ai-provider-spec.ts +++ b/ui/src/api/generated/models/ai-provider-spec.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-provider-status.ts b/ui/src/api/generated/models/ai-provider-status.ts index 5b5b4e8e..5dbfef31 100644 --- a/ui/src/api/generated/models/ai-provider-status.ts +++ b/ui/src/api/generated/models/ai-provider-status.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-provider.ts b/ui/src/api/generated/models/ai-provider.ts index 233a300a..43ebbd86 100644 --- a/ui/src/api/generated/models/ai-provider.ts +++ b/ui/src/api/generated/models/ai-provider.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/caller-plugin-info.ts b/ui/src/api/generated/models/caller-plugin-info.ts index e741f920..6b688623 100644 --- a/ui/src/api/generated/models/caller-plugin-info.ts +++ b/ui/src/api/generated/models/caller-plugin-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/copy-operation.ts b/ui/src/api/generated/models/copy-operation.ts index ffb9d89d..5749469d 100644 --- a/ui/src/api/generated/models/copy-operation.ts +++ b/ui/src/api/generated/models/copy-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/data-content.ts b/ui/src/api/generated/models/data-content.ts index 3c8feaa5..9e3470a8 100644 --- a/ui/src/api/generated/models/data-content.ts +++ b/ui/src/api/generated/models/data-content.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/decision-answer.ts b/ui/src/api/generated/models/decision-answer.ts new file mode 100644 index 00000000..e80daab0 --- /dev/null +++ b/ui/src/api/generated/models/decision-answer.ts @@ -0,0 +1,75 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.26.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface DecisionAnswer + */ +export interface DecisionAnswer { + /** + * + * @type {string} + * @memberof DecisionAnswer + */ + 'choice'?: string; + /** + * + * @type {number} + * @memberof DecisionAnswer + */ + 'confidence'?: number; + /** + * + * @type {{ [key: string]: string; }} + * @memberof DecisionAnswer + */ + 'legend'?: { [key: string]: string; }; + /** + * + * @type {{ [key: string]: number; }} + * @memberof DecisionAnswer + */ + 'probabilities'?: { [key: string]: number; }; + /** + * + * @type {number} + * @memberof DecisionAnswer + */ + 'probability'?: number; + /** + * + * @type {number} + * @memberof DecisionAnswer + */ + 'score'?: number; + /** + * + * @type {string} + * @memberof DecisionAnswer + */ + 'type'?: DecisionAnswerTypeEnum; +} + +export const DecisionAnswerTypeEnum = { + YesNo: 'YES_NO', + Choice: 'CHOICE', + Score: 'SCORE' +} as const; + +export type DecisionAnswerTypeEnum = typeof DecisionAnswerTypeEnum[keyof typeof DecisionAnswerTypeEnum]; + + diff --git a/ui/src/api/generated/models/decision-capability.ts b/ui/src/api/generated/models/decision-capability.ts new file mode 100644 index 00000000..f054e36d --- /dev/null +++ b/ui/src/api/generated/models/decision-capability.ts @@ -0,0 +1,57 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.26.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface DecisionCapability + */ +export interface DecisionCapability { + /** + * + * @type {number} + * @memberof DecisionCapability + */ + 'maxChoiceOptions'?: number; + /** + * + * @type {number} + * @memberof DecisionCapability + */ + 'maxScoreLevels'?: number; + /** + * + * @type {Array} + * @memberof DecisionCapability + */ + 'questionTypes'?: Array; + /** + * + * @type {boolean} + * @memberof DecisionCapability + */ + 'structuredState'?: boolean; +} + +export const DecisionCapabilityQuestionTypesEnum = { + YesNo: 'YES_NO', + Choice: 'CHOICE', + Score: 'SCORE' +} as const; + +export type DecisionCapabilityQuestionTypesEnum = typeof DecisionCapabilityQuestionTypesEnum[keyof typeof DecisionCapabilityQuestionTypesEnum]; + + diff --git a/ui/src/api/generated/models/decision-question.ts b/ui/src/api/generated/models/decision-question.ts new file mode 100644 index 00000000..82cbc02b --- /dev/null +++ b/ui/src/api/generated/models/decision-question.ts @@ -0,0 +1,69 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.26.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Independent questions keyed by caller-selected identifiers + * @export + * @interface DecisionQuestion + */ +export interface DecisionQuestion { + /** + * + * @type {string} + * @memberof DecisionQuestion + */ + 'instructions'?: string; + /** + * + * @type {Array} + * @memberof DecisionQuestion + */ + 'levels'?: Array; + /** + * + * @type {string} + * @memberof DecisionQuestion + */ + 'noCriteria'?: string; + /** + * + * @type {{ [key: string]: string; }} + * @memberof DecisionQuestion + */ + 'options'?: { [key: string]: string; }; + /** + * + * @type {string} + * @memberof DecisionQuestion + */ + 'type'?: DecisionQuestionTypeEnum; + /** + * + * @type {string} + * @memberof DecisionQuestion + */ + 'yesCriteria'?: string; +} + +export const DecisionQuestionTypeEnum = { + YesNo: 'YES_NO', + Choice: 'CHOICE', + Score: 'SCORE' +} as const; + +export type DecisionQuestionTypeEnum = typeof DecisionQuestionTypeEnum[keyof typeof DecisionQuestionTypeEnum]; + + diff --git a/ui/src/api/generated/models/decision-response.ts b/ui/src/api/generated/models/decision-response.ts new file mode 100644 index 00000000..aadcee6c --- /dev/null +++ b/ui/src/api/generated/models/decision-response.ts @@ -0,0 +1,48 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.26.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { DecisionAnswer } from './decision-answer'; +// May contain unused imports in some cases +// @ts-ignore +import type { DecisionUsage } from './decision-usage'; + +/** + * + * @export + * @interface DecisionResponse + */ +export interface DecisionResponse { + /** + * + * @type {{ [key: string]: DecisionAnswer; }} + * @memberof DecisionResponse + */ + 'answers'?: { [key: string]: DecisionAnswer; }; + /** + * + * @type {string} + * @memberof DecisionResponse + */ + 'model'?: string; + /** + * + * @type {DecisionUsage} + * @memberof DecisionResponse + */ + 'usage'?: DecisionUsage; +} + diff --git a/ui/src/api/generated/models/decision-usage.ts b/ui/src/api/generated/models/decision-usage.ts new file mode 100644 index 00000000..cc7eaeaf --- /dev/null +++ b/ui/src/api/generated/models/decision-usage.ts @@ -0,0 +1,36 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.26.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface DecisionUsage + */ +export interface DecisionUsage { + /** + * + * @type {number} + * @memberof DecisionUsage + */ + 'inputTokens'?: number; + /** + * + * @type {number} + * @memberof DecisionUsage + */ + 'outputTokens'?: number; +} + diff --git a/ui/src/api/generated/models/default-model-slots.ts b/ui/src/api/generated/models/default-model-slots.ts index be19146b..6de04509 100644 --- a/ui/src/api/generated/models/default-model-slots.ts +++ b/ui/src/api/generated/models/default-model-slots.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -20,6 +20,12 @@ * @interface DefaultModelSlots */ export interface DefaultModelSlots { + /** + * + * @type {string} + * @memberof DefaultModelSlots + */ + 'decisionModelName'?: string; /** * * @type {string} diff --git a/ui/src/api/generated/models/default-parameter-mapping-info.ts b/ui/src/api/generated/models/default-parameter-mapping-info.ts index 66ab1fce..684a5366 100644 --- a/ui/src/api/generated/models/default-parameter-mapping-info.ts +++ b/ui/src/api/generated/models/default-parameter-mapping-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/discovered-model-item.ts b/ui/src/api/generated/models/discovered-model-item.ts index 48cd41e4..af2af06e 100644 --- a/ui/src/api/generated/models/discovered-model-item.ts +++ b/ui/src/api/generated/models/discovered-model-item.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -154,7 +154,8 @@ export const DiscoveredModelItemAdapterTypeEnum = { OllamaResponses: 'ollama-responses', OllamaMessages: 'ollama-messages', OllamaEmbedding: 'ollama-embedding', - OllamaImage: 'ollama-image' + OllamaImage: 'ollama-image', + TypesafeSystemOne: 'typesafe-system-one' } as const; export type DiscoveredModelItemAdapterTypeEnum = typeof DiscoveredModelItemAdapterTypeEnum[keyof typeof DiscoveredModelItemAdapterTypeEnum]; @@ -179,7 +180,8 @@ export const DiscoveredModelItemModelTypeEnum = { Language: 'language', Embedding: 'embedding', Rerank: 'rerank', - ImageGeneration: 'image-generation' + ImageGeneration: 'image-generation', + Decision: 'decision' } as const; export type DiscoveredModelItemModelTypeEnum = typeof DiscoveredModelItemModelTypeEnum[keyof typeof DiscoveredModelItemModelTypeEnum]; diff --git a/ui/src/api/generated/models/embedding-mappings.ts b/ui/src/api/generated/models/embedding-mappings.ts index a988128a..4f3d5f6d 100644 --- a/ui/src/api/generated/models/embedding-mappings.ts +++ b/ui/src/api/generated/models/embedding-mappings.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/embedding-preview.ts b/ui/src/api/generated/models/embedding-preview.ts index b57bcc8c..f00d6680 100644 --- a/ui/src/api/generated/models/embedding-preview.ts +++ b/ui/src/api/generated/models/embedding-preview.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/embedding-response-metadata.ts b/ui/src/api/generated/models/embedding-response-metadata.ts index 00bb74cc..b424bb77 100644 --- a/ui/src/api/generated/models/embedding-response-metadata.ts +++ b/ui/src/api/generated/models/embedding-response-metadata.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/embedding-usage.ts b/ui/src/api/generated/models/embedding-usage.ts index d242a940..67126a65 100644 --- a/ui/src/api/generated/models/embedding-usage.ts +++ b/ui/src/api/generated/models/embedding-usage.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/embedding-warning.ts b/ui/src/api/generated/models/embedding-warning.ts index d1fdf73e..063a16e5 100644 --- a/ui/src/api/generated/models/embedding-warning.ts +++ b/ui/src/api/generated/models/embedding-warning.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/generation-response-metadata.ts b/ui/src/api/generated/models/generation-response-metadata.ts index 029e555a..fe4c0652 100644 --- a/ui/src/api/generated/models/generation-response-metadata.ts +++ b/ui/src/api/generated/models/generation-response-metadata.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/image-generation-capability.ts b/ui/src/api/generated/models/image-generation-capability.ts index 1e66de41..c1aca969 100644 --- a/ui/src/api/generated/models/image-generation-capability.ts +++ b/ui/src/api/generated/models/image-generation-capability.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/image-generation-mappings.ts b/ui/src/api/generated/models/image-generation-mappings.ts index f65fa219..d6d2a0f1 100644 --- a/ui/src/api/generated/models/image-generation-mappings.ts +++ b/ui/src/api/generated/models/image-generation-mappings.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/image-generation-warning.ts b/ui/src/api/generated/models/image-generation-warning.ts index d4a77ed7..2babde6b 100644 --- a/ui/src/api/generated/models/image-generation-warning.ts +++ b/ui/src/api/generated/models/image-generation-warning.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/image-usage.ts b/ui/src/api/generated/models/image-usage.ts index 7cfd002a..74386c57 100644 --- a/ui/src/api/generated/models/image-usage.ts +++ b/ui/src/api/generated/models/image-usage.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/index.ts b/ui/src/api/generated/models/index.ts index 29f5ec69..c6bd8c8a 100644 --- a/ui/src/api/generated/models/index.ts +++ b/ui/src/api/generated/models/index.ts @@ -10,6 +10,11 @@ export * from './ai-provider-status'; export * from './caller-plugin-info'; export * from './copy-operation'; export * from './data-content'; +export * from './decision-answer'; +export * from './decision-capability'; +export * from './decision-question'; +export * from './decision-response'; +export * from './decision-usage'; export * from './default-model-slots'; export * from './default-parameter-mapping-info'; export * from './discovered-model-item'; @@ -55,6 +60,7 @@ export * from './rerank-warning'; export * from './reset-request'; export * from './selection'; export * from './test-completion-stream-request'; +export * from './test-decision-request'; export * from './test-embedding-request'; export * from './test-embedding-response'; export * from './test-generated-file'; diff --git a/ui/src/api/generated/models/json-patch-inner.ts b/ui/src/api/generated/models/json-patch-inner.ts index 2b1c9d4d..e1ea2224 100644 --- a/ui/src/api/generated/models/json-patch-inner.ts +++ b/ui/src/api/generated/models/json-patch-inner.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/language-capability.ts b/ui/src/api/generated/models/language-capability.ts index c2fbec5b..d37b2e5f 100644 --- a/ui/src/api/generated/models/language-capability.ts +++ b/ui/src/api/generated/models/language-capability.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/language-mappings.ts b/ui/src/api/generated/models/language-mappings.ts index c6ec1a04..91b6945e 100644 --- a/ui/src/api/generated/models/language-mappings.ts +++ b/ui/src/api/generated/models/language-mappings.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/metadata.ts b/ui/src/api/generated/models/metadata.ts index f7da9aeb..7f326d7d 100644 --- a/ui/src/api/generated/models/metadata.ts +++ b/ui/src/api/generated/models/metadata.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-capabilities.ts b/ui/src/api/generated/models/model-capabilities.ts index af6c1cf9..a07c6ec8 100644 --- a/ui/src/api/generated/models/model-capabilities.ts +++ b/ui/src/api/generated/models/model-capabilities.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -13,6 +13,9 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { DecisionCapability } from './decision-capability'; // May contain unused imports in some cases // @ts-ignore import type { ImageGenerationCapability } from './image-generation-capability'; @@ -29,6 +32,12 @@ import type { ModelCapabilitySources } from './model-capability-sources'; * @interface ModelCapabilities */ export interface ModelCapabilities { + /** + * + * @type {DecisionCapability} + * @memberof ModelCapabilities + */ + 'decision'?: DecisionCapability; /** * * @type {ImageGenerationCapability} diff --git a/ui/src/api/generated/models/model-capability-sources.ts b/ui/src/api/generated/models/model-capability-sources.ts index b5489f0b..8e8b60c0 100644 --- a/ui/src/api/generated/models/model-capability-sources.ts +++ b/ui/src/api/generated/models/model-capability-sources.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -20,6 +20,12 @@ * @interface ModelCapabilitySources */ export interface ModelCapabilitySources { + /** + * + * @type {string} + * @memberof ModelCapabilitySources + */ + 'decision'?: ModelCapabilitySourcesDecisionEnum; /** * * @type {string} @@ -34,6 +40,14 @@ export interface ModelCapabilitySources { 'language'?: ModelCapabilitySourcesLanguageEnum; } +export const ModelCapabilitySourcesDecisionEnum = { + Remote: 'remote', + BuiltIn: 'built-in', + Manual: 'manual', + Unknown: 'unknown' +} as const; + +export type ModelCapabilitySourcesDecisionEnum = typeof ModelCapabilitySourcesDecisionEnum[keyof typeof ModelCapabilitySourcesDecisionEnum]; export const ModelCapabilitySourcesImageGenerationEnum = { Remote: 'remote', BuiltIn: 'built-in', diff --git a/ui/src/api/generated/models/model-message-part.ts b/ui/src/api/generated/models/model-message-part.ts index a4e662f4..9b11ccd2 100644 --- a/ui/src/api/generated/models/model-message-part.ts +++ b/ui/src/api/generated/models/model-message-part.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-message.ts b/ui/src/api/generated/models/model-message.ts index 0e404902..6fd6ea8a 100644 --- a/ui/src/api/generated/models/model-message.ts +++ b/ui/src/api/generated/models/model-message.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-option-provider.ts b/ui/src/api/generated/models/model-option-provider.ts index ffd5342e..de99d78f 100644 --- a/ui/src/api/generated/models/model-option-provider.ts +++ b/ui/src/api/generated/models/model-option-provider.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-option-unavailable-detail.ts b/ui/src/api/generated/models/model-option-unavailable-detail.ts index 3d763af2..35d67db6 100644 --- a/ui/src/api/generated/models/model-option-unavailable-detail.ts +++ b/ui/src/api/generated/models/model-option-unavailable-detail.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-option.ts b/ui/src/api/generated/models/model-option.ts index d3e7576e..4e5843f1 100644 --- a/ui/src/api/generated/models/model-option.ts +++ b/ui/src/api/generated/models/model-option.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -120,7 +120,8 @@ export const ModelOptionModelTypeEnum = { Language: 'language', Embedding: 'embedding', Rerank: 'rerank', - ImageGeneration: 'image-generation' + ImageGeneration: 'image-generation', + Decision: 'decision' } as const; export type ModelOptionModelTypeEnum = typeof ModelOptionModelTypeEnum[keyof typeof ModelOptionModelTypeEnum]; diff --git a/ui/src/api/generated/models/model-parameter-definition-info.ts b/ui/src/api/generated/models/model-parameter-definition-info.ts index a68a2cd1..a738ab5e 100644 --- a/ui/src/api/generated/models/model-parameter-definition-info.ts +++ b/ui/src/api/generated/models/model-parameter-definition-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -76,7 +76,8 @@ export const ModelParameterDefinitionInfoModelTypeEnum = { Language: 'language', Embedding: 'embedding', Rerank: 'rerank', - ImageGeneration: 'image-generation' + ImageGeneration: 'image-generation', + Decision: 'decision' } as const; export type ModelParameterDefinitionInfoModelTypeEnum = typeof ModelParameterDefinitionInfoModelTypeEnum[keyof typeof ModelParameterDefinitionInfoModelTypeEnum]; diff --git a/ui/src/api/generated/models/model-parameter-mappings.ts b/ui/src/api/generated/models/model-parameter-mappings.ts index 71810e55..dc6e5b8c 100644 --- a/ui/src/api/generated/models/model-parameter-mappings.ts +++ b/ui/src/api/generated/models/model-parameter-mappings.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/move-operation.ts b/ui/src/api/generated/models/move-operation.ts index b2670fae..f458b49b 100644 --- a/ui/src/api/generated/models/move-operation.ts +++ b/ui/src/api/generated/models/move-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/normalized-usage.ts b/ui/src/api/generated/models/normalized-usage.ts index 9be00d1a..195ccf0a 100644 --- a/ui/src/api/generated/models/normalized-usage.ts +++ b/ui/src/api/generated/models/normalized-usage.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/output-spec.ts b/ui/src/api/generated/models/output-spec.ts index 918f5df1..59393c26 100644 --- a/ui/src/api/generated/models/output-spec.ts +++ b/ui/src/api/generated/models/output-spec.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/parameter-mapping-template-info.ts b/ui/src/api/generated/models/parameter-mapping-template-info.ts index f4a3fd33..69c21a4d 100644 --- a/ui/src/api/generated/models/parameter-mapping-template-info.ts +++ b/ui/src/api/generated/models/parameter-mapping-template-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -145,7 +145,8 @@ export const ParameterMappingTemplateInfoAdapterTypesEnum = { OllamaResponses: 'ollama-responses', OllamaMessages: 'ollama-messages', OllamaEmbedding: 'ollama-embedding', - OllamaImage: 'ollama-image' + OllamaImage: 'ollama-image', + TypesafeSystemOne: 'typesafe-system-one' } as const; export type ParameterMappingTemplateInfoAdapterTypesEnum = typeof ParameterMappingTemplateInfoAdapterTypesEnum[keyof typeof ParameterMappingTemplateInfoAdapterTypesEnum]; @@ -153,7 +154,8 @@ export const ParameterMappingTemplateInfoModelTypeEnum = { Language: 'language', Embedding: 'embedding', Rerank: 'rerank', - ImageGeneration: 'image-generation' + ImageGeneration: 'image-generation', + Decision: 'decision' } as const; export type ParameterMappingTemplateInfoModelTypeEnum = typeof ParameterMappingTemplateInfoModelTypeEnum[keyof typeof ParameterMappingTemplateInfoModelTypeEnum]; diff --git a/ui/src/api/generated/models/provider-model-discovery-response.ts b/ui/src/api/generated/models/provider-model-discovery-response.ts index 701940f9..70fc6126 100644 --- a/ui/src/api/generated/models/provider-model-discovery-response.ts +++ b/ui/src/api/generated/models/provider-model-discovery-response.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/provider-type-info.ts b/ui/src/api/generated/models/provider-type-info.ts index 2d470f6e..0dd4699b 100644 --- a/ui/src/api/generated/models/provider-type-info.ts +++ b/ui/src/api/generated/models/provider-type-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -226,7 +226,8 @@ export const ProviderTypeInfoSupportedAdapterTypesEnum = { OllamaResponses: 'ollama-responses', OllamaMessages: 'ollama-messages', OllamaEmbedding: 'ollama-embedding', - OllamaImage: 'ollama-image' + OllamaImage: 'ollama-image', + TypesafeSystemOne: 'typesafe-system-one' } as const; export type ProviderTypeInfoSupportedAdapterTypesEnum = typeof ProviderTypeInfoSupportedAdapterTypesEnum[keyof typeof ProviderTypeInfoSupportedAdapterTypesEnum]; @@ -244,7 +245,8 @@ export const ProviderTypeInfoSupportedModelTypesEnum = { Language: 'language', Embedding: 'embedding', Rerank: 'rerank', - ImageGeneration: 'image-generation' + ImageGeneration: 'image-generation', + Decision: 'decision' } as const; export type ProviderTypeInfoSupportedModelTypesEnum = typeof ProviderTypeInfoSupportedModelTypesEnum[keyof typeof ProviderTypeInfoSupportedModelTypesEnum]; diff --git a/ui/src/api/generated/models/reasoning-mapping.ts b/ui/src/api/generated/models/reasoning-mapping.ts index c1c26fd8..609c8124 100644 --- a/ui/src/api/generated/models/reasoning-mapping.ts +++ b/ui/src/api/generated/models/reasoning-mapping.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/reasoning-options.ts b/ui/src/api/generated/models/reasoning-options.ts index 2ab3d4e4..ec347b1e 100644 --- a/ui/src/api/generated/models/reasoning-options.ts +++ b/ui/src/api/generated/models/reasoning-options.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/reasoning-value-mapping.ts b/ui/src/api/generated/models/reasoning-value-mapping.ts index 6c4d4f3e..e3237658 100644 --- a/ui/src/api/generated/models/reasoning-value-mapping.ts +++ b/ui/src/api/generated/models/reasoning-value-mapping.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/remove-operation.ts b/ui/src/api/generated/models/remove-operation.ts index 6c413d06..16b57b51 100644 --- a/ui/src/api/generated/models/remove-operation.ts +++ b/ui/src/api/generated/models/remove-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/replace-operation.ts b/ui/src/api/generated/models/replace-operation.ts index e8724043..99d981be 100644 --- a/ui/src/api/generated/models/replace-operation.ts +++ b/ui/src/api/generated/models/replace-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/rerank-mappings.ts b/ui/src/api/generated/models/rerank-mappings.ts index a095f008..4c6afe57 100644 --- a/ui/src/api/generated/models/rerank-mappings.ts +++ b/ui/src/api/generated/models/rerank-mappings.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/rerank-preview.ts b/ui/src/api/generated/models/rerank-preview.ts index c41d8a82..8df35037 100644 --- a/ui/src/api/generated/models/rerank-preview.ts +++ b/ui/src/api/generated/models/rerank-preview.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/rerank-response-metadata.ts b/ui/src/api/generated/models/rerank-response-metadata.ts index 05e0945f..a624cae6 100644 --- a/ui/src/api/generated/models/rerank-response-metadata.ts +++ b/ui/src/api/generated/models/rerank-response-metadata.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/rerank-usage.ts b/ui/src/api/generated/models/rerank-usage.ts index bfc1c651..e1b8674e 100644 --- a/ui/src/api/generated/models/rerank-usage.ts +++ b/ui/src/api/generated/models/rerank-usage.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/rerank-warning.ts b/ui/src/api/generated/models/rerank-warning.ts index e4a052b2..f4e8ef8c 100644 --- a/ui/src/api/generated/models/rerank-warning.ts +++ b/ui/src/api/generated/models/rerank-warning.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/reset-request.ts b/ui/src/api/generated/models/reset-request.ts index 541d21ad..e913a6b5 100644 --- a/ui/src/api/generated/models/reset-request.ts +++ b/ui/src/api/generated/models/reset-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/selection.ts b/ui/src/api/generated/models/selection.ts index c5167bb0..9c38d35f 100644 --- a/ui/src/api/generated/models/selection.ts +++ b/ui/src/api/generated/models/selection.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-completion-stream-request.ts b/ui/src/api/generated/models/test-completion-stream-request.ts index 02714b4b..016ba7c1 100644 --- a/ui/src/api/generated/models/test-completion-stream-request.ts +++ b/ui/src/api/generated/models/test-completion-stream-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-decision-request.ts b/ui/src/api/generated/models/test-decision-request.ts new file mode 100644 index 00000000..74350d69 --- /dev/null +++ b/ui/src/api/generated/models/test-decision-request.ts @@ -0,0 +1,53 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.26.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { DecisionQuestion } from './decision-question'; + +/** + * + * @export + * @interface TestDecisionRequest + */ +export interface TestDecisionRequest { + /** + * Independent questions keyed by caller-selected identifiers + * @type {{ [key: string]: DecisionQuestion; }} + * @memberof TestDecisionRequest + */ + 'questions': { [key: string]: DecisionQuestion; }; + /** + * Text to evaluate, or serialized JSON when stateFormat is JSON + * @type {string} + * @memberof TestDecisionRequest + */ + 'state': string; + /** + * Input format; defaults to TEXT + * @type {string} + * @memberof TestDecisionRequest + */ + 'stateFormat'?: TestDecisionRequestStateFormatEnum; +} + +export const TestDecisionRequestStateFormatEnum = { + Text: 'TEXT', + Json: 'JSON' +} as const; + +export type TestDecisionRequestStateFormatEnum = typeof TestDecisionRequestStateFormatEnum[keyof typeof TestDecisionRequestStateFormatEnum]; + + diff --git a/ui/src/api/generated/models/test-embedding-request.ts b/ui/src/api/generated/models/test-embedding-request.ts index 9f6d4bf5..f09cad95 100644 --- a/ui/src/api/generated/models/test-embedding-request.ts +++ b/ui/src/api/generated/models/test-embedding-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-embedding-response.ts b/ui/src/api/generated/models/test-embedding-response.ts index ed4baa14..de5d7e99 100644 --- a/ui/src/api/generated/models/test-embedding-response.ts +++ b/ui/src/api/generated/models/test-embedding-response.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-generated-file.ts b/ui/src/api/generated/models/test-generated-file.ts index d6c3a386..b6f71672 100644 --- a/ui/src/api/generated/models/test-generated-file.ts +++ b/ui/src/api/generated/models/test-generated-file.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-image-generation-request.ts b/ui/src/api/generated/models/test-image-generation-request.ts index c3cbf07e..8ceb80db 100644 --- a/ui/src/api/generated/models/test-image-generation-request.ts +++ b/ui/src/api/generated/models/test-image-generation-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-image-generation-response.ts b/ui/src/api/generated/models/test-image-generation-response.ts index 2cc90eb4..0fc9af75 100644 --- a/ui/src/api/generated/models/test-image-generation-response.ts +++ b/ui/src/api/generated/models/test-image-generation-response.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-media-content.ts b/ui/src/api/generated/models/test-media-content.ts index 1fd467a1..473dd57c 100644 --- a/ui/src/api/generated/models/test-media-content.ts +++ b/ui/src/api/generated/models/test-media-content.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-object-stream-request.ts b/ui/src/api/generated/models/test-object-stream-request.ts index b6f6f327..080716b9 100644 --- a/ui/src/api/generated/models/test-object-stream-request.ts +++ b/ui/src/api/generated/models/test-object-stream-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-operation.ts b/ui/src/api/generated/models/test-operation.ts index 73dedea5..74efa916 100644 --- a/ui/src/api/generated/models/test-operation.ts +++ b/ui/src/api/generated/models/test-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-rag-options.ts b/ui/src/api/generated/models/test-rag-options.ts index 43d74a04..bcd75115 100644 --- a/ui/src/api/generated/models/test-rag-options.ts +++ b/ui/src/api/generated/models/test-rag-options.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-rag-request.ts b/ui/src/api/generated/models/test-rag-request.ts index 9640e2fd..52179675 100644 --- a/ui/src/api/generated/models/test-rag-request.ts +++ b/ui/src/api/generated/models/test-rag-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-rag-source.ts b/ui/src/api/generated/models/test-rag-source.ts index e0a7519a..790ddc2f 100644 --- a/ui/src/api/generated/models/test-rag-source.ts +++ b/ui/src/api/generated/models/test-rag-source.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-rerank-request.ts b/ui/src/api/generated/models/test-rerank-request.ts index 2728ad7f..d5e1c699 100644 --- a/ui/src/api/generated/models/test-rerank-request.ts +++ b/ui/src/api/generated/models/test-rerank-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-rerank-response.ts b/ui/src/api/generated/models/test-rerank-response.ts index 67130829..3fbfd2f6 100644 --- a/ui/src/api/generated/models/test-rerank-response.ts +++ b/ui/src/api/generated/models/test-rerank-response.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-ui-message-chat-request.ts b/ui/src/api/generated/models/test-ui-message-chat-request.ts index 334dda2e..42d6faa0 100644 --- a/ui/src/api/generated/models/test-ui-message-chat-request.ts +++ b/ui/src/api/generated/models/test-ui-message-chat-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-ui-message.ts b/ui/src/api/generated/models/test-ui-message.ts index 849e5783..12d4b24c 100644 --- a/ui/src/api/generated/models/test-ui-message.ts +++ b/ui/src/api/generated/models/test-ui-message.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/tool-choice.ts b/ui/src/api/generated/models/tool-choice.ts index c6fec7f1..43f901cd 100644 --- a/ui/src/api/generated/models/tool-choice.ts +++ b/ui/src/api/generated/models/tool-choice.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/uimessage-chunk.ts b/ui/src/api/generated/models/uimessage-chunk.ts index 82abd4bd..d62119aa 100644 --- a/ui/src/api/generated/models/uimessage-chunk.ts +++ b/ui/src/api/generated/models/uimessage-chunk.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-breakdown-item.ts b/ui/src/api/generated/models/usage-breakdown-item.ts index 9f9ed541..85a6353a 100644 --- a/ui/src/api/generated/models/usage-breakdown-item.ts +++ b/ui/src/api/generated/models/usage-breakdown-item.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-breakdown.ts b/ui/src/api/generated/models/usage-breakdown.ts index 82eca11c..b82af1eb 100644 --- a/ui/src/api/generated/models/usage-breakdown.ts +++ b/ui/src/api/generated/models/usage-breakdown.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-call-detail.ts b/ui/src/api/generated/models/usage-call-detail.ts index b4b10a1a..e251fb18 100644 --- a/ui/src/api/generated/models/usage-call-detail.ts +++ b/ui/src/api/generated/models/usage-call-detail.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-call-item.ts b/ui/src/api/generated/models/usage-call-item.ts index 05315d78..6550c229 100644 --- a/ui/src/api/generated/models/usage-call-item.ts +++ b/ui/src/api/generated/models/usage-call-item.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-call-page.ts b/ui/src/api/generated/models/usage-call-page.ts index 10bbbe1a..f16c9ef1 100644 --- a/ui/src/api/generated/models/usage-call-page.ts +++ b/ui/src/api/generated/models/usage-call-page.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-error.ts b/ui/src/api/generated/models/usage-error.ts index 72d45158..703c6246 100644 --- a/ui/src/api/generated/models/usage-error.ts +++ b/ui/src/api/generated/models/usage-error.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-execution-record.ts b/ui/src/api/generated/models/usage-execution-record.ts index 9b2b16f6..b4f4b61e 100644 --- a/ui/src/api/generated/models/usage-execution-record.ts +++ b/ui/src/api/generated/models/usage-execution-record.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -120,7 +120,8 @@ export const UsageExecutionRecordUnitKindEnum = { GenerationStep: 'GENERATION_STEP', EmbeddingBatch: 'EMBEDDING_BATCH', Rerank: 'RERANK', - ImageBatch: 'IMAGE_BATCH' + ImageBatch: 'IMAGE_BATCH', + Decision: 'DECISION' } as const; export type UsageExecutionRecordUnitKindEnum = typeof UsageExecutionRecordUnitKindEnum[keyof typeof UsageExecutionRecordUnitKindEnum]; diff --git a/ui/src/api/generated/models/usage-health.ts b/ui/src/api/generated/models/usage-health.ts index 604309b9..68e01d83 100644 --- a/ui/src/api/generated/models/usage-health.ts +++ b/ui/src/api/generated/models/usage-health.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-summary.ts b/ui/src/api/generated/models/usage-summary.ts index ef034196..05ca8e74 100644 --- a/ui/src/api/generated/models/usage-summary.ts +++ b/ui/src/api/generated/models/usage-summary.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-trend-dimension-value.ts b/ui/src/api/generated/models/usage-trend-dimension-value.ts index 5e2171cc..1fcd9feb 100644 --- a/ui/src/api/generated/models/usage-trend-dimension-value.ts +++ b/ui/src/api/generated/models/usage-trend-dimension-value.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-trend-point.ts b/ui/src/api/generated/models/usage-trend-point.ts index 4a8a2a3b..4bdd5889 100644 --- a/ui/src/api/generated/models/usage-trend-point.ts +++ b/ui/src/api/generated/models/usage-trend-point.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.0 + * The version of the OpenAPI document: 2.26.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/composables/workbench/use-decision-test.test.ts b/ui/src/composables/workbench/use-decision-test.test.ts new file mode 100644 index 00000000..87714fb0 --- /dev/null +++ b/ui/src/composables/workbench/use-decision-test.test.ts @@ -0,0 +1,94 @@ +import { aiConsoleApiClient } from '@/api' +import { DecisionQuestionTypeEnum, type ModelOption } from '@/api/generated' +import { buildDecisionRequest, type DecisionQuestionDraft } from '@/utils/decision-workbench' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { computed, effectScope, nextTick, shallowRef } from 'vue' +import { useDecisionTest } from './use-decision-test' + +vi.mock('@/api', () => ({ aiConsoleApiClient: { model: { testModelDecision: vi.fn() } } })) + +describe('decision workbench', () => { + beforeEach(() => vi.clearAllMocks()) + + it('submits mixed questions and keeps the typed response', async () => { + const scope = effectScope() + const test = scope.run(() => useDecisionTest(computed(() => ({ name: 'configured-model' }))))! + const result = { + model: 'jev-version', + answers: { spam: { type: 'YES_NO', probability: 0.12 } }, + } + vi.mocked(aiConsoleApiClient.model.testModelDecision).mockResolvedValue({ + data: result, + } as never) + await test.run() + expect(aiConsoleApiClient.model.testModelDecision).toHaveBeenCalledWith( + { + name: 'configured-model', + testDecisionRequest: expect.objectContaining({ + stateFormat: 'TEXT', + questions: { + spam: { type: 'YES_NO', instructions: '这条评论是否为垃圾广告?' }, + intent: { + type: 'CHOICE', + instructions: '评论的主要意图是什么?', + options: { question: '提问', feedback: '反馈', other: '其他' }, + }, + relevance: { + type: 'SCORE', + instructions: '评论与插件配置主题有多相关?', + levels: ['无关', '部分相关', '高度相关'], + }, + }, + }), + }, + { signal: expect.any(AbortSignal) }, + ) + expect(test.result.value).toBe(result) + expect(test.isLoading.value).toBe(false) + scope.stop() + }) + + it('rejects invalid JSON before requesting and ignores stale responses after model changes', async () => { + const selected = shallowRef({ name: 'first' }) + const scope = effectScope() + const test = scope.run(() => useDecisionTest(computed(() => selected.value)))! + test.stateFormat.value = 'json' + test.stateText.value = '{' + await test.run() + expect(test.error.value).toContain('JSON') + expect(aiConsoleApiClient.model.testModelDecision).not.toHaveBeenCalled() + test.stateText.value = '{"comment":"hello"}' + let resolve!: (value: never) => void + vi.mocked(aiConsoleApiClient.model.testModelDecision).mockReturnValue( + new Promise((done) => { + resolve = done + }), + ) + const pending = test.run() + selected.value = { name: 'second' } + await nextTick() + resolve({ data: { model: 'first', answers: {} } } as never) + await pending + expect(test.result.value).toBeUndefined() + expect(test.isLoading.value).toBe(false) + scope.stop() + }) + + it('rejects duplicate question and option identifiers', () => { + const draft: DecisionQuestionDraft = { + key: 1, + id: 'q', + type: DecisionQuestionTypeEnum.Choice, + instructions: 'Choose', + criteria: 'a=A\na=B', + } + expect(() => buildDecisionRequest('text', 'text', [draft])).toThrow('选项标识') + draft.criteria = 'a=A\nb=B' + expect(() => buildDecisionRequest('text', 'text', [draft, { ...draft, key: 2 }])).toThrow( + '问题标识', + ) + expect(buildDecisionRequest('["first", {"second":true}]', 'json', [draft])).toMatchObject({ + stateFormat: 'JSON', + }) + }) +}) diff --git a/ui/src/composables/workbench/use-decision-test.ts b/ui/src/composables/workbench/use-decision-test.ts new file mode 100644 index 00000000..fcf8761d --- /dev/null +++ b/ui/src/composables/workbench/use-decision-test.ts @@ -0,0 +1,113 @@ +import { aiConsoleApiClient } from '@/api' +import { DecisionQuestionTypeEnum, type DecisionResponse, type ModelOption } from '@/api/generated' +import { buildDecisionRequest, type DecisionQuestionDraft } from '@/utils/decision-workbench' +import { getCurrentScope, onScopeDispose, ref, shallowRef, watch, type ComputedRef } from 'vue' + +export function useDecisionTest(selectedModel: ComputedRef) { + const stateText = shallowRef('这篇文章很有帮助!请问如何配置插件?') + const stateFormat = shallowRef<'text' | 'json'>('text') + const questions = ref([ + { + key: 1, + id: 'spam', + type: DecisionQuestionTypeEnum.YesNo, + instructions: '这条评论是否为垃圾广告?', + criteria: '', + }, + { + key: 2, + id: 'intent', + type: DecisionQuestionTypeEnum.Choice, + instructions: '评论的主要意图是什么?', + criteria: 'question=提问\nfeedback=反馈\nother=其他', + }, + { + key: 3, + id: 'relevance', + type: DecisionQuestionTypeEnum.Score, + instructions: '评论与插件配置主题有多相关?', + criteria: '无关\n部分相关\n高度相关', + }, + ]) + const result = shallowRef() + const error = shallowRef('') + const isLoading = shallowRef(false) + let nextKey = 4 + let controller: AbortController | undefined + let sequence = 0 + + function cancel() { + sequence++ + controller?.abort() + controller = undefined + isLoading.value = false + } + + watch( + () => selectedModel.value?.name, + () => { + cancel() + result.value = undefined + error.value = '' + }, + ) + if (getCurrentScope()) onScopeDispose(cancel) + + function addQuestion() { + const key = nextKey++ + questions.value.push({ + key, + id: `question_${key}`, + type: DecisionQuestionTypeEnum.YesNo, + instructions: '', + criteria: '', + }) + } + + function updateQuestion(question: DecisionQuestionDraft) { + questions.value = questions.value.map((item) => (item.key === question.key ? question : item)) + } + + function removeQuestion(key: number) { + questions.value = questions.value.filter((item) => item.key !== key) + } + + async function run() { + const name = selectedModel.value?.name + if (!name || isLoading.value) return + error.value = '' + result.value = undefined + const runId = ++sequence + try { + const request = buildDecisionRequest(stateText.value, stateFormat.value, questions.value) + controller = new AbortController() + isLoading.value = true + const { data } = await aiConsoleApiClient.model.testModelDecision( + { name, testDecisionRequest: request }, + { signal: controller.signal }, + ) + if (runId === sequence) result.value = data + } catch (cause) { + if (runId === sequence) error.value = (cause as Error).message || '决策请求失败' + } finally { + if (runId === sequence) { + isLoading.value = false + controller = undefined + } + } + } + + return { + stateText, + stateFormat, + questions, + result, + error, + isLoading, + run, + cancel, + addQuestion, + updateQuestion, + removeQuestion, + } +} diff --git a/ui/src/composables/workbench/use-workbench-models.ts b/ui/src/composables/workbench/use-workbench-models.ts index cb1b0092..d334a33a 100644 --- a/ui/src/composables/workbench/use-workbench-models.ts +++ b/ui/src/composables/workbench/use-workbench-models.ts @@ -3,12 +3,13 @@ import { useModelOptionsFetch } from '@/composables/use-model-options-fetch' import { useRouteQuery } from '@vueuse/router' import { computed, shallowRef, watch, type ComputedRef, type ShallowRef } from 'vue' -export type WorkbenchTestMode = 'chat' | 'embedding' | 'rerank' | 'image' | 'rag' +export type WorkbenchTestMode = 'chat' | 'embedding' | 'rerank' | 'image' | 'rag' | 'decision' export interface WorkbenchModels { availableOnly: ShallowRef selectedModelName: ReturnType> testMode: ShallowRef + decisionModels: ComputedRef chatModels: ComputedRef embeddingModels: ComputedRef rerankModels: ComputedRef @@ -22,6 +23,7 @@ export interface WorkbenchModels { } const modelTypeByMode: Record = { + decision: ModelOptionModelTypeEnum.Decision, chat: ModelOptionModelTypeEnum.Language, embedding: ModelOptionModelTypeEnum.Embedding, rerank: ModelOptionModelTypeEnum.Rerank, @@ -48,6 +50,7 @@ export function useWorkbenchModels(): WorkbenchModels { computed(() => (modelOptions.value || []).filter((model) => model.name && model.modelType === type), ) + const decisionModels = modelsByType(ModelOptionModelTypeEnum.Decision) const chatModels = modelsByType(ModelOptionModelTypeEnum.Language) const embeddingModels = modelsByType(ModelOptionModelTypeEnum.Embedding) const rerankModels = modelsByType(ModelOptionModelTypeEnum.Rerank) @@ -55,6 +58,8 @@ export function useWorkbenchModels(): WorkbenchModels { const activeModelType = computed(() => modelTypeByMode[testMode.value]) const activeModels = computed(() => { switch (activeModelType.value) { + case ModelOptionModelTypeEnum.Decision: + return decisionModels.value case ModelOptionModelTypeEnum.Embedding: return embeddingModels.value case ModelOptionModelTypeEnum.Rerank: @@ -75,6 +80,9 @@ export function useWorkbenchModels(): WorkbenchModels { if (!items) return const selected = items.find((item) => item.name === selectedModelName.value) switch (selected?.modelType) { + case ModelOptionModelTypeEnum.Decision: + testMode.value = 'decision' + break case ModelOptionModelTypeEnum.Embedding: testMode.value = 'embedding' break @@ -107,6 +115,7 @@ export function useWorkbenchModels(): WorkbenchModels { availableOnly, selectedModelName, testMode, + decisionModels, chatModels, embeddingModels, rerankModels, diff --git a/ui/src/formkit/ai-model-selector.ts b/ui/src/formkit/ai-model-selector.ts index 00f51a1d..5977e6cd 100644 --- a/ui/src/formkit/ai-model-selector.ts +++ b/ui/src/formkit/ai-model-selector.ts @@ -90,6 +90,8 @@ export function modelFeatureLabel(feature: string): string { export function modelTypeLabel(type?: string): string { switch (type) { + case ModelOptionModelTypeEnum.Decision: + return '决策' case ModelOptionModelTypeEnum.Language: return '语言' case ModelOptionModelTypeEnum.Embedding: diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 26359d40..e4a86825 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -10,6 +10,7 @@ export const MODEL_TYPE_OPTIONS = [ { label: 'Embedding', value: AiModelSpecModelTypeEnum.Embedding }, { label: 'Rerank', value: AiModelSpecModelTypeEnum.Rerank }, { label: '图像生成', value: AiModelSpecModelTypeEnum.ImageGeneration }, + { label: '决策模型', value: AiModelSpecModelTypeEnum.Decision }, ] as const export const MODEL_FEATURE_OPTIONS = [ diff --git a/ui/src/utils/capabilities.ts b/ui/src/utils/capabilities.ts index c5757d7e..cd9b9beb 100644 --- a/ui/src/utils/capabilities.ts +++ b/ui/src/utils/capabilities.ts @@ -10,7 +10,10 @@ import { ModelCapabilitySourcesLanguageEnum, } from '@/api/generated' -export type RequiredModelCapabilities = Pick +export type RequiredModelCapabilities = Pick< + ModelCapabilities, + 'language' | 'imageGeneration' | 'decision' +> export type RequiredModelCapabilitiesValue = RequiredModelCapabilities | string | undefined export function normalizeRequiredCapabilities(value?: RequiredModelCapabilitiesValue) { @@ -22,6 +25,7 @@ export function normalizeRequiredCapabilities(value?: RequiredModelCapabilitiesV return trimmed || undefined } const normalized = pruneEmpty({ + decision: pruneEmpty(value.decision), language: pruneEmpty(value.language), imageGeneration: pruneEmpty(value.imageGeneration), }) @@ -30,6 +34,14 @@ export function normalizeRequiredCapabilities(value?: RequiredModelCapabilitiesV export function capabilitySummaryLabels(capabilities?: ModelCapabilities) { const labels: string[] = [] + const decisionLabels: Record = { + YES_NO: '是非概率', + CHOICE: '单选', + SCORE: '评分', + } + for (const type of capabilities?.decision?.questionTypes || []) { + labels.push(decisionLabels[type] || type) + } const language = capabilities?.language const image = capabilities?.imageGeneration @@ -93,6 +105,14 @@ export function capabilityUnavailableDetailsLabel(model: ModelOption) { export function capabilityPathLabel(path?: string) { switch (path) { + case 'decision.questionTypes': + return '支持决策问题类型' + case 'decision.structuredState': + return '支持 JSON 状态' + case 'decision.maxChoiceOptions': + return '支持所需选项数量' + case 'decision.maxScoreLevels': + return '支持所需评分等级' case 'language.imageInput': return '支持图片识别' case 'language.fileInput': diff --git a/ui/src/utils/decision-workbench.ts b/ui/src/utils/decision-workbench.ts new file mode 100644 index 00000000..e4fcb2ed --- /dev/null +++ b/ui/src/utils/decision-workbench.ts @@ -0,0 +1,77 @@ +import { + DecisionQuestionTypeEnum, + type DecisionQuestion, + type TestDecisionRequest, +} from '@/api/generated' + +export interface DecisionQuestionDraft { + key: number + id: string + type: DecisionQuestionTypeEnum + instructions: string + criteria: string +} + +export const DECISION_QUESTION_TYPES = [ + { value: DecisionQuestionTypeEnum.YesNo, label: '是非概率' }, + { value: DecisionQuestionTypeEnum.Choice, label: '单选' }, + { value: DecisionQuestionTypeEnum.Score, label: '评分' }, +] + +export function buildDecisionRequest( + stateText: string, + stateFormat: 'text' | 'json', + drafts: DecisionQuestionDraft[], +): TestDecisionRequest { + if (!stateText.trim()) throw new Error('请输入待判断的内容') + let state: unknown = stateText + if (stateFormat === 'json') { + try { + state = JSON.parse(stateText) + } catch { + throw new Error('状态不是有效的 JSON') + } + if (state === null || (typeof state !== 'object' && typeof state !== 'string')) { + throw new Error('JSON 状态必须是字符串、对象或数组') + } + } + if (!drafts.length) throw new Error('请至少添加一个问题') + const entries: [string, DecisionQuestion][] = [] + const ids = new Set() + for (const draft of drafts) { + const id = draft.id.trim() + if (!id || ids.has(id)) throw new Error('问题标识不能为空或重复') + ids.add(id) + if (!draft.instructions.trim()) throw new Error(`请填写问题「${id}」的判断说明`) + const question: DecisionQuestion = { type: draft.type, instructions: draft.instructions.trim() } + const lines = draft.criteria + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + if (draft.type === DecisionQuestionTypeEnum.Choice) { + if (lines.length < 2) throw new Error(`问题「${id}」至少需要两个选项`) + const options = lines.map((line) => { + const separator = line.indexOf('=') + return separator < 0 + ? [line, line] + : [line.slice(0, separator).trim(), line.slice(separator + 1).trim()] + }) + if ( + options.some(([key]) => !key) || + new Set(options.map(([key]) => key)).size !== options.length + ) { + throw new Error(`问题「${id}」的选项标识不能为空或重复`) + } + question.options = Object.fromEntries(options) + } else if (draft.type === DecisionQuestionTypeEnum.Score) { + if (lines.length < 2) throw new Error(`问题「${id}」至少需要两个评分等级`) + question.levels = lines + } + entries.push([id, question]) + } + return { + state: stateText, + stateFormat: stateFormat === 'json' ? 'JSON' : 'TEXT', + questions: Object.fromEntries(entries), + } +} diff --git a/ui/src/utils/usage.ts b/ui/src/utils/usage.ts index 8ca830d2..83823170 100644 --- a/ui/src/utils/usage.ts +++ b/ui/src/utils/usage.ts @@ -1,6 +1,6 @@ +import type { AiModel, AiProvider, CallerPluginInfo } from '@/api/generated' import { utils } from '@halo-dev/ui-shared' import { fromDate, toCalendarDate } from '@internationalized/date' -import type { AiModel, AiProvider, CallerPluginInfo } from '@/api/generated' const UNKNOWN_TEXT = '未知' export type UsageTrendResolution = 'HOUR' | 'DAY' @@ -28,6 +28,7 @@ export const USAGE_QUALITY_OPTIONS = [ // 用量统计的 modelType 过滤值与后端 ModelType.name() 存储值一致, // 与 AiModel.spec.modelType 的 kebab 形式不同。 export const USAGE_MODEL_TYPE_OPTIONS = [ + { label: '决策模型', value: 'DECISION' }, { label: '语言模型', value: 'LANGUAGE' }, { label: 'Embedding', value: 'EMBEDDING' }, { label: 'Rerank', value: 'RERANK' }, @@ -35,6 +36,7 @@ export const USAGE_MODEL_TYPE_OPTIONS = [ ] as const export const USAGE_OPERATION_OPTIONS = [ + { label: '决策判断', value: 'decision.decide' }, { label: '文本生成', value: 'language.generateText' }, { label: '流式文本生成', value: 'language.streamText' }, { label: '文本嵌入', value: 'embedding.embed' }, @@ -48,6 +50,7 @@ const QUALITY_LABELS = toLabelMap(USAGE_QUALITY_OPTIONS) const OPERATION_LABELS = toLabelMap(USAGE_OPERATION_OPTIONS) const UNIT_KIND_LABELS: Record = { + DECISION: '决策判断', GENERATION_STEP: '生成步骤', EMBEDDING_BATCH: '嵌入批次', RERANK: '重排序', @@ -221,10 +224,7 @@ export function usageDisplayNames( } /** 维度值展示名;空值显示「未知」,未知名称回退为原始标识。 */ -export function usageDimensionLabel( - names: Map, - key?: string | null, -) { +export function usageDimensionLabel(names: Map, key?: string | null) { if (!key) return UNKNOWN_TEXT return names.get(key) || key } diff --git a/ui/src/views/DefaultModelSlotsView.vue b/ui/src/views/DefaultModelSlotsView.vue index c334c1f8..9571d4f8 100644 --- a/ui/src/views/DefaultModelSlotsView.vue +++ b/ui/src/views/DefaultModelSlotsView.vue @@ -49,6 +49,7 @@ const slotDefinitions: Array<{ }, }, }, + { key: 'decisionModelName', label: '默认决策模型', modelType: ModelOptionModelTypeEnum.Decision }, ] const queryClient = useQueryClient() @@ -68,6 +69,7 @@ watch( slots, (value) => { Object.assign(formState, { + decisionModelName: value?.decisionModelName || '', languageModelName: value?.languageModelName || '', embeddingModelName: value?.embeddingModelName || '', rerankModelName: value?.rerankModelName || '', @@ -85,6 +87,7 @@ const modelOptionCount = computed(() => modelOptions.value?.length || 0) function normalizeSlots(): DefaultModelSlots { return { + decisionModelName: formState.decisionModelName || undefined, languageModelName: formState.languageModelName || undefined, embeddingModelName: formState.embeddingModelName || undefined, rerankModelName: formState.rerankModelName || undefined, diff --git a/ui/src/views/ModelTestWorkbenchView.vue b/ui/src/views/ModelTestWorkbenchView.vue index 1e20a5c6..09dedc64 100644 --- a/ui/src/views/ModelTestWorkbenchView.vue +++ b/ui/src/views/ModelTestWorkbenchView.vue @@ -1,5 +1,6 @@ + + diff --git a/ui/src/views/components/workbench/DecisionResults.test.ts b/ui/src/views/components/workbench/DecisionResults.test.ts new file mode 100644 index 00000000..0c952fe8 --- /dev/null +++ b/ui/src/views/components/workbench/DecisionResults.test.ts @@ -0,0 +1,31 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it } from 'vitest' +import DecisionResults from './DecisionResults.vue' + +describe('decision results', () => { + it('shows yes probability, a fractional score and the complete distribution', () => { + const wrapper = mount(DecisionResults, { + props: { + result: { + model: 'jev-version', + usage: { inputTokens: 12 }, + answers: { + spam: { type: 'YES_NO', probability: 0.12 }, + relevance: { + type: 'SCORE', + score: 0.25, + confidence: 0.5, + probabilities: { 0: 0.75, 1: 0.25 }, + legend: { 0: '低', 1: '高' }, + }, + }, + }, + }, + }) + expect(wrapper.text()).toContain('12.0%') + expect(wrapper.text()).toContain('评分:0.25') + expect(wrapper.text()).toContain('75.0%') + expect(wrapper.text()).toContain('输出 未知') + expect(wrapper.text()).toContain('不等于正确率') + }) +}) diff --git a/ui/src/views/components/workbench/DecisionResults.vue b/ui/src/views/components/workbench/DecisionResults.vue new file mode 100644 index 00000000..58885372 --- /dev/null +++ b/ui/src/views/components/workbench/DecisionResults.vue @@ -0,0 +1,50 @@ + + + diff --git a/ui/src/views/components/workbench/DecisionTestPanel.vue b/ui/src/views/components/workbench/DecisionTestPanel.vue new file mode 100644 index 00000000..da264410 --- /dev/null +++ b/ui/src/views/components/workbench/DecisionTestPanel.vue @@ -0,0 +1,79 @@ + + + diff --git a/ui/src/views/components/workbench/WorkbenchToolbar.vue b/ui/src/views/components/workbench/WorkbenchToolbar.vue index 664df771..9fdfd87c 100644 --- a/ui/src/views/components/workbench/WorkbenchToolbar.vue +++ b/ui/src/views/components/workbench/WorkbenchToolbar.vue @@ -32,6 +32,7 @@ const testModeTabs: Tab[] = [ { label: 'Rerank', value: 'rerank', icon: RiStackLine }, { label: '图片', value: 'image', icon: RiImageLine }, { label: 'RAG', value: 'rag', icon: RiStackLine }, + { label: '决策', value: 'decision', icon: RiStackLine }, ] From ec67376c70baf3ca3204a66ea85effeb4539f2a2 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 28 Oct 2026 14:16:59 +0800 Subject: [PATCH 02/10] fix: make decision result diagnostics optional --- README.md | 2 +- README_EN.md | 2 +- api-docs/openapi/v3_0/aiFoundationApis.json | 15 +++ .../capability/DecisionCapability.java | 17 +++- .../aifoundation/decision/DecisionAnswer.java | 8 +- .../endpoint/ModelOptionQuery.java | 12 ++- .../provider/typesafe/TypeSafeProvider.java | 9 +- .../capability/ModelCapabilityMatcher.java | 10 ++ .../service/decision/DecisionModelImpl.java | 4 +- .../service/decision/DecisionValidation.java | 30 +++--- .../ModelOptionConsoleEndpointTest.java | 42 ++++++++ .../typesafe/TypeSafeDecisionClientTest.java | 20 ++++ .../service/AiModelServiceImplTest.java | 11 ++- .../service/decision/DecisionModelTest.java | 78 ++++++++++++++- dev/en/sdk-core/decision-models.md | 36 ++++++- dev/zh-CN/sdk-core/decision-models.md | 27 +++++- ui/packages/sdk/src/decision.test.ts | 96 +++++++++++++++++++ ui/packages/sdk/src/decision.ts | 53 ++++++---- .../generated/models/decision-capability.ts | 30 ++++++ ui/src/utils/capabilities.test.ts | 30 ++++++ ui/src/utils/capabilities.ts | 17 ++++ .../workbench/DecisionResults.test.ts | 37 +++++++ .../components/workbench/DecisionResults.vue | 14 ++- 23 files changed, 541 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 3e1552f3..c799009e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Halo 官方 AI 能力平台,统一接入主流大模型,为插件生态提 ## 功能特性 - **多提供商支持**:内置 OpenAI、OpenRouter、DeepSeek、月之暗面 Kimi、硅基流动、阿里云百炼、豆包、文心一言、智谱开放平台、Ollama、OpenAI 兼容、AIHubMix、Gitee 模力方舟、MiniMax、腾讯云 TokenHub 和 Xiaomi MiMo -- **决策模型**:接入 TypeSafe / Jev,统一提供是非概率、单选和评分,以及决策测试工作台 +- **决策模型**:提供供应商中立的概率判断、单选、评分 API 和决策工作台,内置 TypeSafe / Jev 适配器 - **统一模型管理**:在 Halo 控制台管理 Provider、API Key Secret、语言 / Embedding / Rerank / 图像生成模型、Adapter 与模型能力 - **模型自动发现**:从 Provider 拉取模型,并保留远程声明、内建能力和管理员覆盖的来源信息 - **统一参数映射**:按 Provider 或模型配置采样、重试、推理、Embedding、Rerank 和图像参数到供应方字段的映射 diff --git a/README_EN.md b/README_EN.md index 88ff5122..e6fe4648 100644 --- a/README_EN.md +++ b/README_EN.md @@ -10,7 +10,7 @@ ecosystem. ## Features -- **Decision models**: TypeSafe / Jev integration with typed probability, choice, score results and a decision workbench. +- **Decision models**: Provider-neutral probability, choice, and score APIs with a decision workbench and a built-in TypeSafe / Jev adapter. - **Multiple AI providers**: Built-in support for OpenAI, OpenRouter, DeepSeek, Moonshot Kimi, SiliconFlow, Alibaba Cloud Model Studio, Doubao, ERNIE, Zhipu AI, Ollama, OpenAI-compatible providers, AIHubMix, Gitee AI, MiniMax, Tencent Cloud TokenHub, and Xiaomi MiMo diff --git a/api-docs/openapi/v3_0/aiFoundationApis.json b/api-docs/openapi/v3_0/aiFoundationApis.json index af9e1140..f0ce57c8 100644 --- a/api-docs/openapi/v3_0/aiFoundationApis.json +++ b/api-docs/openapi/v3_0/aiFoundationApis.json @@ -2184,6 +2184,12 @@ "DecisionCapability" : { "type" : "object", "properties" : { + "choiceConfidence" : { + "type" : "boolean" + }, + "choiceProbabilities" : { + "type" : "boolean" + }, "maxChoiceOptions" : { "type" : "integer", "format" : "int32" @@ -2199,6 +2205,15 @@ "enum" : [ "YES_NO", "CHOICE", "SCORE" ] } }, + "scoreConfidence" : { + "type" : "boolean" + }, + "scoreLegend" : { + "type" : "boolean" + }, + "scoreProbabilities" : { + "type" : "boolean" + }, "structuredState" : { "type" : "boolean" } diff --git a/api/src/main/java/run/halo/aifoundation/capability/DecisionCapability.java b/api/src/main/java/run/halo/aifoundation/capability/DecisionCapability.java index 04758ffb..b565ac42 100644 --- a/api/src/main/java/run/halo/aifoundation/capability/DecisionCapability.java +++ b/api/src/main/java/run/halo/aifoundation/capability/DecisionCapability.java @@ -7,7 +7,10 @@ import lombok.NoArgsConstructor; import run.halo.aifoundation.decision.DecisionQuestionType; -/** Capabilities of a decision adapter. Null limits are unknown, not unlimited. */ +/** + * Capabilities of a decision adapter. Null values mean unknown. + * Output flags indicate supported information, not a guarantee that every response includes it. + */ @Data @Builder @NoArgsConstructor @@ -17,4 +20,16 @@ public class DecisionCapability { private Boolean structuredState; private Integer maxChoiceOptions; private Integer maxScoreLevels; + private Boolean choiceProbabilities; + private Boolean choiceConfidence; + private Boolean scoreProbabilities; + private Boolean scoreConfidence; + private Boolean scoreLegend; + + /** Retains the constructor used before optional output capabilities were introduced. */ + public DecisionCapability(List questionTypes, Boolean structuredState, + Integer maxChoiceOptions, Integer maxScoreLevels) { + this(questionTypes, structuredState, maxChoiceOptions, maxScoreLevels, + null, null, null, null, null); + } } diff --git a/api/src/main/java/run/halo/aifoundation/decision/DecisionAnswer.java b/api/src/main/java/run/halo/aifoundation/decision/DecisionAnswer.java index b740dc87..ed412a94 100644 --- a/api/src/main/java/run/halo/aifoundation/decision/DecisionAnswer.java +++ b/api/src/main/java/run/halo/aifoundation/decision/DecisionAnswer.java @@ -17,12 +17,12 @@ public class DecisionAnswer { private Double probability; /** Selected CHOICE option identifier. */ private String choice; - /** SCORE's probability-weighted level index, which may be fractional. */ + /** SCORE level index in [0, number of levels - 1], which may be fractional. */ private Double score; - /** Distribution over option identifiers or zero-based score level indices. */ + /** Optional complete distribution over option identifiers or zero-based score level indices. */ private Map probabilities; - /** Distribution-derived certainty for CHOICE/SCORE, absent for YES_NO. */ + /** Optional provider-reported CHOICE/SCORE confidence in [0, 1]; semantics vary by provider. */ private Double confidence; - /** SCORE level indices and descriptions. */ + /** Optional SCORE level indices and descriptions. */ private Map legend; } diff --git a/app/src/main/java/run/halo/aifoundation/endpoint/ModelOptionQuery.java b/app/src/main/java/run/halo/aifoundation/endpoint/ModelOptionQuery.java index 86aecf7e..bd62346f 100644 --- a/app/src/main/java/run/halo/aifoundation/endpoint/ModelOptionQuery.java +++ b/app/src/main/java/run/halo/aifoundation/endpoint/ModelOptionQuery.java @@ -218,12 +218,20 @@ private static DecisionCapability parseDecisionCapability(Map value) { return null; } rejectUnknownKeys(value, "decision", List.of("questionTypes", "structuredState", - "maxChoiceOptions", "maxScoreLevels")); + "maxChoiceOptions", "maxScoreLevels", "choiceProbabilities", "choiceConfidence", + "scoreProbabilities", "scoreConfidence", "scoreLegend")); var types = stringList(value.get("questionTypes"), "decision.questionTypes"); var builder = DecisionCapability.builder() .structuredState(booleanValue(value.get("structuredState"), "decision.structuredState")) .maxChoiceOptions(integerValue(value.get("maxChoiceOptions"), "decision.maxChoiceOptions")) - .maxScoreLevels(integerValue(value.get("maxScoreLevels"), "decision.maxScoreLevels")); + .maxScoreLevels(integerValue(value.get("maxScoreLevels"), "decision.maxScoreLevels")) + .choiceProbabilities(booleanValue(value.get("choiceProbabilities"), + "decision.choiceProbabilities")) + .choiceConfidence(booleanValue(value.get("choiceConfidence"), "decision.choiceConfidence")) + .scoreProbabilities(booleanValue(value.get("scoreProbabilities"), + "decision.scoreProbabilities")) + .scoreConfidence(booleanValue(value.get("scoreConfidence"), "decision.scoreConfidence")) + .scoreLegend(booleanValue(value.get("scoreLegend"), "decision.scoreLegend")); if (types != null) { builder.questionTypes(types.stream().map(type -> { try { diff --git a/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java index b285b14e..3e9b7679 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java +++ b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java @@ -25,8 +25,13 @@ public class TypeSafeProvider extends AbstractAiProviderType { @Override public DecisionCapability decisionCapabilities() { - return new DecisionCapability( - List.of(DecisionQuestionType.values()), true, 255, 10); + return DecisionCapability.builder() + .questionTypes(List.of(DecisionQuestionType.YES_NO, DecisionQuestionType.CHOICE, + DecisionQuestionType.SCORE)) + .structuredState(true).maxChoiceOptions(255).maxScoreLevels(10) + .choiceProbabilities(true).choiceConfidence(true) + .scoreProbabilities(true).scoreConfidence(true).scoreLegend(true) + .build(); } @Override diff --git a/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityMatcher.java b/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityMatcher.java index d2a7e009..aaaea8ae 100644 --- a/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityMatcher.java +++ b/app/src/main/java/run/halo/aifoundation/service/capability/ModelCapabilityMatcher.java @@ -61,6 +61,16 @@ private void matchDecision(DecisionCapability actual, expected.getMaxChoiceOptions(), issues); matchMinInteger("decision.maxScoreLevels", actual == null ? null : actual.getMaxScoreLevels(), expected.getMaxScoreLevels(), issues); + matchBoolean("decision.choiceProbabilities", + actual == null ? null : actual.getChoiceProbabilities(), expected.getChoiceProbabilities(), issues); + matchBoolean("decision.choiceConfidence", + actual == null ? null : actual.getChoiceConfidence(), expected.getChoiceConfidence(), issues); + matchBoolean("decision.scoreProbabilities", + actual == null ? null : actual.getScoreProbabilities(), expected.getScoreProbabilities(), issues); + matchBoolean("decision.scoreConfidence", + actual == null ? null : actual.getScoreConfidence(), expected.getScoreConfidence(), issues); + matchBoolean("decision.scoreLegend", + actual == null ? null : actual.getScoreLegend(), expected.getScoreLegend(), issues); if (expected.getQuestionTypes() != null && !expected.getQuestionTypes().isEmpty() && (actual == null || actual.getQuestionTypes() == null || !actual.getQuestionTypes().containsAll(expected.getQuestionTypes()))) { diff --git a/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java index 0ec3466c..fefb0a24 100644 --- a/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java +++ b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java @@ -36,7 +36,9 @@ private static DecisionCapability copyCapabilities(DecisionCapability capabiliti capabilities.getQuestionTypes() == null ? null : List.copyOf(capabilities.getQuestionTypes()), capabilities.getStructuredState(), capabilities.getMaxChoiceOptions(), - capabilities.getMaxScoreLevels()); + capabilities.getMaxScoreLevels(), capabilities.getChoiceProbabilities(), + capabilities.getChoiceConfidence(), capabilities.getScoreProbabilities(), + capabilities.getScoreConfidence(), capabilities.getScoreLegend()); } @Override diff --git a/app/src/main/java/run/halo/aifoundation/service/decision/DecisionValidation.java b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionValidation.java index a51d1283..d2046f3d 100644 --- a/app/src/main/java/run/halo/aifoundation/service/decision/DecisionValidation.java +++ b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionValidation.java @@ -125,7 +125,7 @@ public static void response(DecisionRequest request, DecisionResponse response) switch (question.getType()) { case YES_NO -> probability(answer.getProbability()); case CHOICE -> { - distribution(answer, question.getOptions().keySet()); + optionalDiagnostics(answer, question.getOptions().keySet()); valid(answer.getChoice() != null && question.getOptions().containsKey(answer.getChoice()), "Decision response selected an unknown option"); @@ -133,27 +133,33 @@ public static void response(DecisionRequest request, DecisionResponse response) case SCORE -> { var keys = IntStream.range(0, question.getLevels().size()) .mapToObj(Integer::toString).collect(Collectors.toCollection(LinkedHashSet::new)); - distribution(answer, keys); + optionalDiagnostics(answer, keys); var score = answer.getScore(); valid(score != null && Double.isFinite(score) && score >= 0 && score <= question.getLevels().size() - 1, "Decision score is outside the requested levels"); - valid(answer.getLegend() != null && answer.getLegend().keySet().equals(keys) - && answer.getLegend().values().stream().allMatch(v -> v != null), - "Decision score legend does not match the requested levels"); + if (answer.getLegend() != null) { + valid(answer.getLegend().keySet().equals(keys) + && answer.getLegend().values().stream().allMatch(v -> v != null), + "Decision score legend does not match the requested levels"); + } } } }); } - private static void distribution(DecisionAnswer answer, Set keys) { + private static void optionalDiagnostics(DecisionAnswer answer, Set keys) { var probabilities = answer.getProbabilities(); - valid(probabilities != null && probabilities.keySet().equals(keys), - "Decision probabilities do not match the requested options"); - probabilities.values().forEach(DecisionValidation::probability); - valid(Math.abs(probabilities.values().stream().mapToDouble(Double::doubleValue).sum() - 1) - <= 0.001, "Decision probabilities must sum to one"); - probability(answer.getConfidence()); + if (probabilities != null) { + valid(probabilities.keySet().equals(keys), + "Decision probabilities do not match the requested options"); + probabilities.values().forEach(DecisionValidation::probability); + valid(Math.abs(probabilities.values().stream().mapToDouble(Double::doubleValue).sum() - 1) + <= 0.001, "Decision probabilities must sum to one"); + } + if (answer.getConfidence() != null) { + probability(answer.getConfidence()); + } } private static void probability(Double value) { diff --git a/app/src/test/java/run/halo/aifoundation/endpoint/ModelOptionConsoleEndpointTest.java b/app/src/test/java/run/halo/aifoundation/endpoint/ModelOptionConsoleEndpointTest.java index 8eb47b6a..7e2329f6 100644 --- a/app/src/test/java/run/halo/aifoundation/endpoint/ModelOptionConsoleEndpointTest.java +++ b/app/src/test/java/run/halo/aifoundation/endpoint/ModelOptionConsoleEndpointTest.java @@ -16,6 +16,8 @@ import org.junit.jupiter.api.Test; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Flux; +import run.halo.aifoundation.capability.DecisionCapability; +import run.halo.aifoundation.decision.DecisionQuestionType; import run.halo.aifoundation.extension.AiModel; import run.halo.aifoundation.extension.AiProvider; import run.halo.aifoundation.provider.AiProviderType; @@ -325,6 +327,46 @@ private URI capabilitiesUri(String prefixQuery, String requiredCapabilities) { return URI.create("/model-options?" + query); } + @Test + void decisionOutputCapabilitiesDistinguishSupportedUnsupportedAndUnknown() { + var labelProvider = providerType("Label provider", null); + when(labelProvider.decisionCapabilities()).thenReturn(DecisionCapability.builder() + .questionTypes(List.of(DecisionQuestionType.CHOICE, DecisionQuestionType.SCORE)) + .choiceProbabilities(false).choiceConfidence(false).scoreProbabilities(false) + .scoreConfidence(false).scoreLegend(false).build()); + var unknownProvider = providerType("Unknown provider", null); + when(unknownProvider.decisionCapabilities()).thenReturn(new DecisionCapability( + List.of(DecisionQuestionType.CHOICE), false, 2, null)); + when(providerClientCache.getProviderTypeMap()).thenReturn(Map.of( + "typesafe", new TypeSafeProvider(), "label-only", labelProvider, "unknown", unknownProvider)); + mockData(List.of( + model("jev", "typesafe", "jev-latest", "Jev", ModelType.DECISION, true, List.of()), + model("label", "label-only", "labels", "Labels", ModelType.DECISION, true, List.of()), + model("unknown", "unknown", "unknown", "Unknown", ModelType.DECISION, true, List.of())), + List.of( + provider("typesafe", "TypeSafe", "typesafe", true, AiProvider.AiProviderStatus.Phase.OK), + provider("label-only", "Labels", "label-only", true, AiProvider.AiProviderStatus.Phase.OK), + provider("unknown", "Unknown", "unknown", true, AiProvider.AiProviderStatus.Phase.OK))); + + webTestClient.get().uri("/model-options?modelType=decision") + .exchange().expectStatus().isOk().expectBodyList(ModelOption.class).hasSize(3); + webTestClient.get().uri(capabilitiesUri(""" + {"decision":{"choiceProbabilities":true,"choiceConfidence":true, + "scoreProbabilities":true,"scoreConfidence":true,"scoreLegend":true}} + """)) + .exchange().expectStatus().isOk().expectBodyList(ModelOption.class) + .value(options -> assertThat(options).extracting(ModelOption::getName).containsExactly("jev")); + webTestClient.get().uri(capabilitiesUri(""" + {"decision":{"choiceProbabilities":false}} + """)) + .exchange().expectStatus().isOk().expectBodyList(ModelOption.class) + .value(options -> assertThat(options).extracting(ModelOption::getName).containsExactly("label")); + webTestClient.get().uri(capabilitiesUri(""" + {"decision":{"scoreConfidence":"true"}} + """)) + .exchange().expectStatus().isBadRequest(); + } + @Test void decisionOptionsExposeAdapterLimitsAndFilterRequiredCapabilities() { when(providerClientCache.getProviderTypeMap()) diff --git a/app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java b/app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java index 5d15dac3..564feb59 100644 --- a/app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java +++ b/app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java @@ -128,6 +128,26 @@ void preservesMissingUsageAsUnknownAndMapsYesNoCriteria() { assertThat(requestBody.get()).contains("\"true\":\"Time-sensitive\"", "\"false\":\"Can wait\""); } + @Test + void stillRejectsMissingTypeSafeProtocolFields() { + var question = DecisionQuestion.choice("Route?", Map.of("a", "A", "b", "B")); + response.set(""" + {"model":"jev-version","answers":{"q":{"type":"choice","choice":"a", + "probabilities":{"a":0.8,"b":0.2}}}} + """); + StepVerifier.create(client().decide(DecisionRequest.builder().state("message") + .questions(Map.of("q", question)).build())) + .expectError(IllegalStateException.class).verify(); + + response.set(""" + {"model":"jev-version","answers":{"q":{"type":"score","score":0.2, + "confidence":0.6,"probabilities":{"0":0.8,"1":0.2}}}} + """); + StepVerifier.create(client().decide(DecisionRequest.builder().state("message") + .questions(Map.of("q", DecisionQuestion.score("Score?", List.of("low", "high")))).build())) + .expectError(IllegalStateException.class).verify(); + } + @Test void propagatesProviderRateLimitErrors() { status.set(429); diff --git a/app/src/test/java/run/halo/aifoundation/service/AiModelServiceImplTest.java b/app/src/test/java/run/halo/aifoundation/service/AiModelServiceImplTest.java index 08f68a9b..a1efebd5 100644 --- a/app/src/test/java/run/halo/aifoundation/service/AiModelServiceImplTest.java +++ b/app/src/test/java/run/halo/aifoundation/service/AiModelServiceImplTest.java @@ -134,7 +134,16 @@ void decisionModel_resolvesDefaultResourceNameAndKeepsProviderCapabilities() { when(providerClientCache.getOrCreateDecisionClient(provider, "sk-test", ProviderModelRef.from(model))) .thenReturn(request -> Mono.empty()); StepVerifier.create(service.decisionModel()) - .assertNext(resolved -> assertThat(resolved.capabilities().getMaxScoreLevels()).isEqualTo(10)) + .assertNext(resolved -> { + assertThat(resolved.capabilities().getMaxScoreLevels()).isEqualTo(10); + assertThat(resolved.capabilities().getChoiceProbabilities()).isTrue(); + assertThat(resolved.capabilities().getChoiceConfidence()).isTrue(); + assertThat(resolved.capabilities().getScoreProbabilities()).isTrue(); + assertThat(resolved.capabilities().getScoreConfidence()).isTrue(); + assertThat(resolved.capabilities().getScoreLegend()).isTrue(); + resolved.capabilities().setChoiceConfidence(false); + assertThat(resolved.capabilities().getChoiceConfidence()).isTrue(); + }) .verifyComplete(); verify(client).fetch(AiModel.class, "decision-resource"); } diff --git a/app/src/test/java/run/halo/aifoundation/service/decision/DecisionModelTest.java b/app/src/test/java/run/halo/aifoundation/service/decision/DecisionModelTest.java index 1500907d..cfd80875 100644 --- a/app/src/test/java/run/halo/aifoundation/service/decision/DecisionModelTest.java +++ b/app/src/test/java/run/halo/aifoundation/service/decision/DecisionModelTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import run.halo.aifoundation.capability.DecisionCapability; import run.halo.aifoundation.chat.GenerationTimeouts; import run.halo.aifoundation.decision.DecisionAnswer; import run.halo.aifoundation.decision.DecisionQuestion; @@ -21,13 +22,83 @@ import run.halo.aifoundation.exception.DecisionCancelledException; import run.halo.aifoundation.exception.DecisionTimeoutException; import run.halo.aifoundation.provider.support.ProviderDecisionClient; -import run.halo.aifoundation.provider.typesafe.TypeSafeProvider; import run.halo.aifoundation.service.observation.UsageExecutionObserver; class DecisionModelTest { private final ProviderDecisionClient client = mock(ProviderDecisionClient.class); private final DecisionModelImpl model = new DecisionModelImpl(client, - new UsageExecutionObserver(), new TypeSafeProvider().decisionCapabilities()); + new UsageExecutionObserver(), new DecisionCapability( + List.of(DecisionQuestionType.YES_NO, DecisionQuestionType.CHOICE, + DecisionQuestionType.SCORE), true, 512, 4)); + + @Test + void acceptsLabelAndScoreOnlyResultsWithoutInventingDiagnostics() { + var request = mixedRequest(); + var response = new DecisionResponse("another-provider-model", Map.of( + "route", DecisionAnswer.builder().type(DecisionQuestionType.CHOICE).choice("review").build(), + "rating", DecisionAnswer.builder().type(DecisionQuestionType.SCORE).score(0.0).build()), null); + when(client.decide(any())).thenReturn(Mono.just(response)); + + StepVerifier.create(model.decide(request)).assertNext(result -> { + assertThat(result).isSameAs(response); + assertThat(result.getAnswers().get("route").getChoice()).isEqualTo("review"); + assertThat(result.getAnswers().get("rating").getScore()).isZero(); + result.getAnswers().values().forEach(answer -> { + assertThat(answer.getProbabilities()).isNull(); + assertThat(answer.getConfidence()).isNull(); + assertThat(answer.getLegend()).isNull(); + }); + }).verifyComplete(); + } + + @Test + void optionalDiagnosticsAreIndependent() { + for (var score : List.of( + DecisionAnswer.builder().type(DecisionQuestionType.SCORE).score(0.5) + .confidence(0.0).build(), + DecisionAnswer.builder().type(DecisionQuestionType.SCORE).score(0.5) + .probabilities(Map.of("0", 0.5, "1", 0.5)).build(), + DecisionAnswer.builder().type(DecisionQuestionType.SCORE).score(0.5) + .legend(Map.of("0", "low", "1", "high")).build())) { + var response = new DecisionResponse("another-provider-model", Map.of( + "route", DecisionAnswer.builder().type(DecisionQuestionType.CHOICE) + .choice("review").confidence(0.0).build(), "rating", score), null); + when(client.decide(any())).thenReturn(Mono.just(response)); + StepVerifier.create(model.decide(mixedRequest())) + .expectNext(response).verifyComplete(); + } + } + + @Test + void stillRejectsInvalidScoresAndMalformedOptionalFields() { + for (var score : List.of( + DecisionAnswer.builder().type(DecisionQuestionType.SCORE).build(), + DecisionAnswer.builder().type(DecisionQuestionType.SCORE).score(2.0).build(), + DecisionAnswer.builder().type(DecisionQuestionType.SCORE).score(Double.NaN).build(), + DecisionAnswer.builder().type(DecisionQuestionType.SCORE).score(0.5).confidence(-0.1).build(), + DecisionAnswer.builder().type(DecisionQuestionType.SCORE).score(0.5) + .confidence(Double.POSITIVE_INFINITY).build(), + DecisionAnswer.builder().type(DecisionQuestionType.SCORE).score(0.5) + .probabilities(Map.of("0", 1.0)).build(), + DecisionAnswer.builder().type(DecisionQuestionType.SCORE).score(0.5) + .probabilities(Map.of("0", 0.1, "1", 0.1)).build(), + DecisionAnswer.builder().type(DecisionQuestionType.SCORE).score(0.5) + .legend(Map.of("0", "low")).build())) { + when(client.decide(any())).thenReturn(Mono.just(new DecisionResponse("model", Map.of( + "route", DecisionAnswer.builder().type(DecisionQuestionType.CHOICE) + .choice("review").build(), "rating", score), null))); + StepVerifier.create(model.decide(mixedRequest())) + .expectError(IllegalStateException.class).verify(); + } + } + + private static DecisionRequest mixedRequest() { + return DecisionRequest.builder().state("message") + .questions(Map.of("route", DecisionQuestion.choice("Route?", + Map.of("review", "Review", "publish", "Publish")), + "rating", DecisionQuestion.score("Rating?", List.of("low", "high")))) + .build(); + } @Test void validatesBeforeCallingProvider() { @@ -88,7 +159,8 @@ void honoursCancellationAndTimeout() { void capabilitySnapshotsCannotMutateRuntimeLimits() { var capability = model.capabilities(); capability.setMaxScoreLevels(100); - assertThat(model.capabilities().getMaxScoreLevels()).isEqualTo(10); + assertThat(model.capabilities().getMaxScoreLevels()).isEqualTo(4); + assertThat(model.capabilities().getChoiceConfidence()).isNull(); } private static DecisionRequest request() { diff --git a/dev/en/sdk-core/decision-models.md b/dev/en/sdk-core/decision-models.md index 61ff80ad..f0592a11 100644 --- a/dev/en/sdk-core/decision-models.md +++ b/dev/en/sdk-core/decision-models.md @@ -3,7 +3,9 @@ [简体中文](../../zh-CN/sdk-core/decision-models.md) | English Decision models answer closed questions about shared text or JSON state. Consumer plugins own thresholds -and subsequent actions. These APIs are new in the development build and require matching plugin and +and subsequent actions. `DecisionModel` is provider-neutral; TypeSafe / Jev is the first built-in +provider. Other adapters can reuse model resolution, the workbench, and usage recording. +These APIs are new in the development build and require matching plugin and Java/npm SDK versions; the published `1.0.0` API does not include them. ## Configure TypeSafe / Jev @@ -41,17 +43,41 @@ Decision types live in `run.halo.aifoundation.decision`. Resolve the service wit `ExtensionGetter`; `modelName` means `AiModel.metadata.name`. Null/blank or the no-argument overload uses the default decision model. Request metadata stays in local audit records and is not sent upstream. -`YES_NO.probability` is the probability of yes, not a boolean. `CHOICE` preserves its selected option, -distribution and confidence. `SCORE` preserves its fractional, zero-based level score, legend, -distribution and confidence. Confidence measures the distribution's concentration, not correctness; +`YES_NO.probability` is the probability of yes, not a boolean. `CHOICE` requires a selected option; +`SCORE` requires a finite level index in `[0, number of levels - 1]`, which may be fractional and need +not be calculated from a distribution. Both accept optional `probabilities` and `confidence`; a score +also accepts an optional `legend`. These fields are independent and may be absent or null. Missing +diagnostics are never synthesized. When present, a distribution must cover every option and sum to one, +and a legend must cover every score level. + +Confidence is a provider-reported value in `[0, 1]` whose meaning depends on the provider. TypeSafe +uses distribution concentration; this is not a universal confidence definition or a correctness guarantee. YES_NO has no separate confidence. Unknown token counts remain null. The response reports the served model. -The runtime validates request fields, provider limits, response question IDs/types and distributions. +The runtime validates request fields, provider limits, response question IDs/types, and any supplied diagnostics. `model.capabilities()` returns supported question types and limits. Selectors can filter by `modelType: decision` and `requiredCapabilities.decision`. A total timeout uses `GenerationTimeouts` and raises `DecisionTimeoutException`. Subscription cancellation cancels HTTP work; cancellation tokens are checked before and after inference and raise `DecisionCancelledException`. +`DecisionCapability` also exposes `choiceProbabilities`, `choiceConfidence`, `scoreProbabilities`, +`scoreConfidence`, and `scoreLegend`: true means supported, false means unsupported, and null means +unknown. For example, require `requiredCapabilities.decision.choiceProbabilities: true` to select +models capable of returning choice distributions. Supported does not mean present in every response; +consumers must still check for absent values. + +## Other providers + +Add an internal `AiProviderType` implementation and a `decision` adapter implementing +`buildDecisionClient(...)` and `decisionCapabilities()`. Map the provider's protocol to the public +request and result types. Label-only and score-only providers do not need to fabricate diagnostics +or change the shared inference runtime. A boolean-only classifier can use a two-option `CHOICE`; +`YES_NO` retains its probability contract rather than converting booleans to invented probabilities. + +Keep protocol-required fields in the adapter's validation. The TypeSafe client still requires its +distributions, confidence, and score legends; the optional public contract does not hide a malformed +TypeSafe response. + ## Browser SDK Expose your own authenticated consumer-plugin endpoint accepting `DecisionRequest` and returning diff --git a/dev/zh-CN/sdk-core/decision-models.md b/dev/zh-CN/sdk-core/decision-models.md index 36a171a2..77537814 100644 --- a/dev/zh-CN/sdk-core/decision-models.md +++ b/dev/zh-CN/sdk-core/decision-models.md @@ -3,6 +3,8 @@ 简体中文 | [English](../../en/sdk-core/decision-models.md) 决策模型针对共享内容回答封闭问题,返回概率判断、候选选择或有序评分。业务插件决定阈值和后续动作。 +`DecisionModel` 是供应商中立的公共契约,TypeSafe / Jev 是首个内置提供商;其他提供商可以通过独立 +适配器接入相同的模型解析、工作台和用量统计。 这些 API 是本次开发新增能力,使用时需要包含此功能的 AI Foundation 插件和 Java/npm SDK;已发布的 `1.0.0` API 不包含它们。 @@ -47,9 +49,13 @@ return extensionGetter.getEnabledExtension(AiModelService.class) `ExtensionGetter` 获取服务。`metadata` 仅供本地审计,不会发送给提供商。 - `YES_NO` 的 `probability` 是“是”的概率,不会自动转为布尔值。TypeSafe 的 `noul` 在适配器内部转换。 -- `CHOICE` 保留 `choice`、完整 `probabilities` 和 `confidence`。 -- `SCORE` 保留从 0 开始的浮点评分、等级 `legend`、概率分布和置信度。 -- `confidence` 描述分布的集中程度,不等于答案正确率;`YES_NO` 没有单独的置信度字段。 +- `CHOICE` 必须返回请求中的一个 `choice`;`probabilities` 和 `confidence` 各自可选。 +- `SCORE` 必须返回 `[0, 等级数 - 1]` 内的 `score`,允许小数,不要求由概率加权计算。 + 等级 `legend`、`probabilities` 和 `confidence` 各自可选。 +- 附加字段缺失或为 `null` 时保持缺失,不填零、不制造分布,也不从分数反推置信度。 + 如果提供概率分布,必须包含全部选项且总和为 1;如果提供图例,必须覆盖全部评分等级。 +- `confidence` 是提供商返回的 `[0, 1]` 数值,含义取决于提供商,不能统一解释为答案正确率。 + TypeSafe 的置信度描述分布集中程度;其他提供商可以采用不同定义。`YES_NO` 没有单独的置信度字段。 - `response.model` 保留实际服务模型,token 数量未知时保持 `null`,不能按零用量处理。 - 请求验证先于推理;返回结果的问题 ID、类型、选项、数值范围和概率分布也会校验。 @@ -58,6 +64,21 @@ return extensionGetter.getEnabledExtension(AiModelService.class) 检查,抛出 `DecisionCancelledException`。通过 `model.capabilities()` 查看问题类型和数量限制。 模型选择器可使用 `modelType: decision` 和 `requiredCapabilities.decision` 过滤。 +`DecisionCapability` 还声明 `choiceProbabilities`、`choiceConfidence`、`scoreProbabilities`、 +`scoreConfidence` 和 `scoreLegend`:`true` 表示支持,`false` 表示不支持,`null` 表示未知。 +例如,要求 `requiredCapabilities.decision.choiceProbabilities: true` 可以筛选能够返回单选概率分布的 +模型。声明支持不保证每次响应均包含该字段,业务代码仍需判断是否存在。 + +## 新增其他提供商 + +在插件内部新增 `AiProviderType` 实现和对应的 `decision` 适配器,提供 `buildDecisionClient(...)` +与 `decisionCapabilities()`,将供应方协议映射成公共请求和结果。只返回标签或评分的提供商无需补造 +诊断数据,也无需修改公共推理流程。二元分类若只返回布尔结果,可用两个选项的 `CHOICE` 表达; +`YES_NO` 保留“是”的概率契约,不把布尔值伪装成概率。 + +提供商协议的强制字段在适配器内校验。例如 TypeSafe 本身要求返回的分布、置信度和评分图例仍由 +TypeSafe 客户端严格解析;公共层放宽可选字段不会掩盖它的异常响应。 + ## 浏览器调用自己的插件接口 消费插件可以接收 `DecisionRequest`、调用 `AiModelService`,并把 `DecisionResponse` 作为 JSON 返回。 diff --git a/ui/packages/sdk/src/decision.test.ts b/ui/packages/sdk/src/decision.test.ts index f9ed4742..cd7ebb4c 100644 --- a/ui/packages/sdk/src/decision.test.ts +++ b/ui/packages/sdk/src/decision.test.ts @@ -92,3 +92,99 @@ describe('decision client', () => { await expect(client.decide(mixed)).rejects.toBeInstanceOf(AIUIProtocolError) }) }) + +describe('optional decision diagnostics', () => { + const mixed: DecisionRequest = { + state: 'message', + questions: { + route: { type: 'CHOICE', instructions: 'Route?', options: { a: 'A', b: 'B' } }, + rating: { type: 'SCORE', instructions: 'Rating?', levels: ['low', 'high'] }, + }, + } + + it.each([ + {}, + { probabilities: null, confidence: null, legend: null }, + { confidence: 0 }, + { probabilities: { '0': 0.5, '1': 0.5 } }, + { legend: { '0': 'low', '1': 'high' } }, + ])('accepts independent score diagnostics without filling absent values: %j', async (fields) => { + const response = { + model: 'another-provider-model', + answers: { + route: { type: 'CHOICE', choice: 'a' }, + rating: { type: 'SCORE', score: 0, ...fields }, + }, + } + const client = createDecisionClient({ + api: '/decision', + fetch: vi.fn().mockResolvedValue(Response.json(response)), + }) + expect(await client.decide(mixed)).toEqual(response) + }) + + it.each([ + { score: null }, + { score: 2 }, + { confidence: '0.5' }, + { confidence: -0.1 }, + { probabilities: {} }, + { probabilities: { '0': 0.1, '1': 0.1 } }, + { probabilities: { '0': -0.1, '1': 1.1 } }, + { probabilities: { '0': '0.5', '1': 0.5 } }, + { legend: {} }, + { legend: { '0': 'low', '1': null } }, + ])('rejects invalid scores or malformed diagnostics when supplied: %j', async (fields) => { + const response = { + model: 'another-provider-model', + answers: { + route: { type: 'CHOICE', choice: 'a' }, + rating: { type: 'SCORE', score: 0, ...fields }, + }, + } + const client = createDecisionClient({ + api: '/decision', + fetch: vi.fn().mockResolvedValue(Response.json(response)), + }) + await expect(client.decide(mixed)).rejects.toBeInstanceOf(AIUIProtocolError) + }) + + it.each([ + { choice: null }, + { choice: 'unknown' }, + { confidence: 1.1 }, + { probabilities: { a: 1 } }, + ])('rejects malformed choices without requiring other diagnostics: %j', async (fields) => { + const client = createDecisionClient({ + api: '/decision', + fetch: vi.fn().mockResolvedValue( + Response.json({ + model: 'another-provider-model', + answers: { + route: { type: 'CHOICE', choice: 'a', ...fields }, + rating: { type: 'SCORE', score: 0 }, + }, + }), + ), + }) + await expect(client.decide(mixed)).rejects.toBeInstanceOf(AIUIProtocolError) + }) + + it.each([{ confidence: 0 }, { probabilities: { a: 0.8, b: 0.2 } }])( + 'accepts choice confidence and distribution independently: %j', + async (fields) => { + const response = { + model: 'another-provider-model', + answers: { + route: { type: 'CHOICE', choice: 'a', ...fields }, + rating: { type: 'SCORE', score: 0 }, + }, + } + const client = createDecisionClient({ + api: '/decision', + fetch: vi.fn().mockResolvedValue(Response.json(response)), + }) + expect(await client.decide(mixed)).toEqual(response) + }, + ) +}) diff --git a/ui/packages/sdk/src/decision.ts b/ui/packages/sdk/src/decision.ts index 8854ace5..18a105ed 100644 --- a/ui/packages/sdk/src/decision.ts +++ b/ui/packages/sdk/src/decision.ts @@ -17,13 +17,18 @@ export type DecisionQuestion = export type DecisionAnswer = | { type: 'YES_NO'; probability: number } - | { type: 'CHOICE'; choice: string; probabilities: Record; confidence: number } + | { + type: 'CHOICE' + choice: string + probabilities?: Record | null + confidence?: number | null + } | { type: 'SCORE' score: number - probabilities: Record - confidence: number - legend: Record + probabilities?: Record | null + confidence?: number | null + legend?: Record | null } export interface DecisionRequest { @@ -121,17 +126,21 @@ function validateDecisionResponse( question.type === 'CHOICE' ? Object.keys(question.options) : question.levels.map((_, index) => String(index)) - assert( - object(answer.probabilities) && sameKeys(answer.probabilities, keys), - `Invalid distribution for ${id}`, - ) - const values = Object.values(answer.probabilities) - assert(values.every(probability), `Invalid probabilities for ${id}`) - assert( - Math.abs(values.reduce((sum, item) => sum + item, 0) - 1) <= 0.001, - `Probabilities must sum to one for ${id}`, - ) - assert(probability(answer.confidence), `Invalid confidence for ${id}`) + if (answer.probabilities != null) { + assert( + object(answer.probabilities) && sameKeys(answer.probabilities, keys), + `Invalid distribution for ${id}`, + ) + const values = Object.values(answer.probabilities) + assert(values.every(probability), `Invalid probabilities for ${id}`) + assert( + Math.abs(values.reduce((sum, item) => sum + item, 0) - 1) <= 0.001, + `Probabilities must sum to one for ${id}`, + ) + } + if (answer.confidence != null) { + assert(probability(answer.confidence), `Invalid confidence for ${id}`) + } if (question.type === 'CHOICE') { assert( typeof answer.choice === 'string' && keys.includes(answer.choice), @@ -145,12 +154,14 @@ function validateDecisionResponse( answer.score <= question.levels.length - 1, `Invalid score for ${id}`, ) - assert( - object(answer.legend) && - sameKeys(answer.legend, keys) && - Object.values(answer.legend).every((item) => typeof item === 'string'), - `Invalid score legend for ${id}`, - ) + if (answer.legend != null) { + assert( + object(answer.legend) && + sameKeys(answer.legend, keys) && + Object.values(answer.legend).every((item) => typeof item === 'string'), + `Invalid score legend for ${id}`, + ) + } } } if (value.usage != null) { diff --git a/ui/src/api/generated/models/decision-capability.ts b/ui/src/api/generated/models/decision-capability.ts index f054e36d..37ab275f 100644 --- a/ui/src/api/generated/models/decision-capability.ts +++ b/ui/src/api/generated/models/decision-capability.ts @@ -20,6 +20,18 @@ * @interface DecisionCapability */ export interface DecisionCapability { + /** + * + * @type {boolean} + * @memberof DecisionCapability + */ + 'choiceConfidence'?: boolean; + /** + * + * @type {boolean} + * @memberof DecisionCapability + */ + 'choiceProbabilities'?: boolean; /** * * @type {number} @@ -38,6 +50,24 @@ export interface DecisionCapability { * @memberof DecisionCapability */ 'questionTypes'?: Array; + /** + * + * @type {boolean} + * @memberof DecisionCapability + */ + 'scoreConfidence'?: boolean; + /** + * + * @type {boolean} + * @memberof DecisionCapability + */ + 'scoreLegend'?: boolean; + /** + * + * @type {boolean} + * @memberof DecisionCapability + */ + 'scoreProbabilities'?: boolean; /** * * @type {boolean} diff --git a/ui/src/utils/capabilities.test.ts b/ui/src/utils/capabilities.test.ts index eb4b2759..4bcb2d4f 100644 --- a/ui/src/utils/capabilities.test.ts +++ b/ui/src/utils/capabilities.test.ts @@ -7,6 +7,36 @@ import { } from './capabilities' describe('capability helpers', () => { + it('shows only declared decision diagnostics and preserves false filter values', () => { + expect( + capabilitySummaryLabels({ + decision: { + questionTypes: ['CHOICE', 'SCORE'], + choiceProbabilities: false, + scoreProbabilities: true, + choiceConfidence: undefined, + scoreConfidence: false, + }, + }), + ).toEqual(['单选', '评分', '概率分布']) + expect( + normalizeRequiredCapabilities({ + decision: { + choiceProbabilities: false, + scoreConfidence: true, + }, + }), + ).toBe('{"decision":{"choiceProbabilities":false,"scoreConfidence":true}}') + expect( + capabilityUnavailableDetailsLabel({ + unavailableDetails: [ + { path: 'decision.choiceProbabilities' }, + { path: 'decision.scoreConfidence' }, + ], + } as ModelOption), + ).toBe('缺少能力:单选概率分布、评分置信度') + }) + it('normalizes structured required capabilities into JSON', () => { expect( normalizeRequiredCapabilities({ diff --git a/ui/src/utils/capabilities.ts b/ui/src/utils/capabilities.ts index cd9b9beb..26a8cb37 100644 --- a/ui/src/utils/capabilities.ts +++ b/ui/src/utils/capabilities.ts @@ -42,6 +42,13 @@ export function capabilitySummaryLabels(capabilities?: ModelCapabilities) { for (const type of capabilities?.decision?.questionTypes || []) { labels.push(decisionLabels[type] || type) } + const decision = capabilities?.decision + if (decision?.choiceProbabilities === true || decision?.scoreProbabilities === true) { + labels.push('概率分布') + } + if (decision?.choiceConfidence === true || decision?.scoreConfidence === true) { + labels.push('置信度') + } const language = capabilities?.language const image = capabilities?.imageGeneration @@ -113,6 +120,16 @@ export function capabilityPathLabel(path?: string) { return '支持所需选项数量' case 'decision.maxScoreLevels': return '支持所需评分等级' + case 'decision.choiceProbabilities': + return '单选概率分布' + case 'decision.choiceConfidence': + return '单选置信度' + case 'decision.scoreProbabilities': + return '评分概率分布' + case 'decision.scoreConfidence': + return '评分置信度' + case 'decision.scoreLegend': + return '评分等级图例' case 'language.imageInput': return '支持图片识别' case 'language.fileInput': diff --git a/ui/src/views/components/workbench/DecisionResults.test.ts b/ui/src/views/components/workbench/DecisionResults.test.ts index 0c952fe8..40b7b6c5 100644 --- a/ui/src/views/components/workbench/DecisionResults.test.ts +++ b/ui/src/views/components/workbench/DecisionResults.test.ts @@ -3,6 +3,43 @@ import { describe, expect, it } from 'vitest' import DecisionResults from './DecisionResults.vue' describe('decision results', () => { + it('renders label and score only results without fabricating probability or confidence', () => { + const wrapper = mount(DecisionResults, { + props: { + result: { + model: 'label-provider', + answers: { + route: { type: 'CHOICE', choice: 'review' }, + rating: { type: 'SCORE', score: 0 }, + }, + }, + }, + }) + expect(wrapper.text()).toContain('选择:review') + expect(wrapper.text()).toContain('评分:0') + expect(wrapper.text()).not.toContain('置信度') + expect(wrapper.text()).not.toContain('%') + expect(wrapper.find('dl').exists()).toBe(false) + }) + + it('shows zero confidence and score legends independently of distributions', () => { + const wrapper = mount(DecisionResults, { + props: { + result: { + model: 'another-provider', + answers: { + route: { type: 'CHOICE', choice: 'review', confidence: 0 }, + rating: { type: 'SCORE', score: 0.5, legend: { '0': '低', '1': '高' } }, + }, + }, + }, + }) + expect(wrapper.text()).toContain('置信度:0.0%') + expect(wrapper.text()).toContain('含义由提供商定义') + expect(wrapper.findAll('dt').map((element) => element.text())).toEqual(['0', '1']) + expect(wrapper.findAll('dd').map((element) => element.text())).toEqual(['低', '高']) + }) + it('shows yes probability, a fractional score and the complete distribution', () => { const wrapper = mount(DecisionResults, { props: { diff --git a/ui/src/views/components/workbench/DecisionResults.vue b/ui/src/views/components/workbench/DecisionResults.vue index 58885372..b4ef43d2 100644 --- a/ui/src/views/components/workbench/DecisionResults.vue +++ b/ui/src/views/components/workbench/DecisionResults.vue @@ -30,9 +30,9 @@ const percent = (value: number) => `${(value * 100).toFixed(1)}%` }}
- 置信度:{{ percent(answer.confidence) }}(反映分布的确定程度,不等于正确率) + 置信度:{{ percent(answer.confidence) }}(含义由提供商定义,不等于正确率)
-
+
`${(value * 100).toFixed(1)}%`
{{ percent(probability) }}
+
+
+
{{ level }}
+
{{ label }}
+
+
From 88412a5547b616d8709b703957ba2f8bb222feaa Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 28 Oct 2026 15:52:36 +0800 Subject: [PATCH 03/10] fix: add TypeSafe provider icon Bundle the icon published by https://typesafe.ai and expose it through provider metadata. --- .../provider/typesafe/TypeSafeProvider.java | 5 +++++ .../main/resources/static/brands/typesafe.png | Bin 0 -> 4311 bytes 2 files changed, 5 insertions(+) create mode 100644 app/src/main/resources/static/brands/typesafe.png diff --git a/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java index 3e9b7679..23052073 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java +++ b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java @@ -49,6 +49,11 @@ public String getDescription() { return "Jev 决策模型,支持文本与 JSON 状态的概率判断、单选和评分。"; } + @Override + public String getIconUrl() { + return "/plugins/ai-foundation/assets/static/brands/typesafe.png"; + } + @Override public String getWebsiteUrl() { return "https://typesafe.ai"; diff --git a/app/src/main/resources/static/brands/typesafe.png b/app/src/main/resources/static/brands/typesafe.png new file mode 100644 index 0000000000000000000000000000000000000000..45107c7d09d7e53da800df531d92a5eaafd08f99 GIT binary patch literal 4311 zcmW+)c{J3G_nytp7$ekRe1?x@4BmJVq6R}VL$Yrnp-3f$q=}kA%uvh>X|tpV5!oWC zER!WAS>vrN*-4hsg7owK{c-Pop68rWyu_ngaibT}j>juD4IAW{@E$r%EHa{d>h z@ExV@c-!_)(R8$TwXr`HG3E4p%JKQMQ^S-~!%kLEP}l+8)Z)A5j0*;V_8OM|s_0^l zBp=yn5sq?paDjLz{>N#kFX+C$qlkr*y&@qH3EBSz6mlnf_l}5+vhj>^Id?MZ!g1OO zi0!H1v&VBQvjQOyls1KA=^8WgE$6>GUyG5p3D7{LSAW+W9zB0(R>C(H!To6o-ci~l!ht#x`b<3E07^Sc1rT+daRN0%!~Lk;#Q5{KlBZu>C& z!Z4Mgq;lq09hxsZSt@i;h!fHh0R>0GqtZxfX=&V$*B|g`-TkG$VBCOu22CmFM#`+) z?*Y!Op00V-8v@j0GphOvQmNlW*7W1B(6Fr!?y$@}t1(-q0+r5pJw3 zpIA%ppc3RaAe~YdJrHsh#wF_`vidnn=4$3Jw3#|Y%S{QO`2OwA| zB_rs=P``m^;aHa`FA%`Pu zStqaNZn-B^0U1GSj7WVW9-Z7r-V!{pgVD4o{p-Jbnr_6~5RoAJQNuMIXz9b1Oz|KU zy6gHWzU2Z}C@B3>zu;6*%b=Qc2Xdo$?o#>dK{W-k=Uzt4$7{J&Ll$AiB?#v`LwAD( zo>wx6nw*wb*R8a6mZ)|JZKg`XH(ca-EOh*foyL?mlr_>KkljSjb246A*3 zm52S{Bmo|kN%a>00pkaVTE)d5{)c04QzmQ?Z7foX1l;{lp%y=kMV^0_*8;p!JbN4{ z+F#t@C;~y$4S!4i&jGu5A<$YTbp+qZx}4`Mvl~Uw6s^EM9~PA)SE;(c!0zYWgH{^E za~7Op3CdFB`>OIaTu++OPdBs);Ae%xTO1(6L_ms6yJS%6*>mLw$J^i&LN60g(bRyc z#DnI5SRPWDz!{`BX0aJWrN7B753lphDG+A1cEE1tCLU7AfB9op||I&oab|{XVc=@YQB# z^4iT7ZQ%B?Js#JII%`D&ao~5{L@WjJ<(6?{`^7Hit~gt-b!bFmup*V2dD*se@d*Sn ze0bq3^6xHK$*(uJ+@1mu{jv;xDx*K{#M_h0NBl zgF*S>FXK28iStdT?t>_z`OJ+Hdmw72vJYfJR`pNr5e2xb=Fy*?9LqIN^YRnplv&;< zY$>*F>Jy3~yiJMg`v}HHC-=w^12Pim5Z>;Wr{@xhZzf)dQN}9ze-XPmX-QUq;sN5>b9$=0mA^Sz||?7}mIp8p2)Vk&L9{{*L^@ z7!he8-b6}AlRbXtH3g6Kspa1llsOkkcZ}y%g~qmi)IV$=CLH8}=C!VF@jM^?PWi@&-A>S2;&sF}+=;RE ztV67h|GYJ=$6j98PiHn>7xtX-`er?vl+PW&cBF!t^yQz<=I;rjkC|H;1+)r+9(!t! z_7`kN9T*W-?f#=ItOov@2hKfmn;6X)fhuMNqvn}9m5afDo}Su>kx-b;KiA6b!!tNR z$%niuBXu#;J}JQ$hgmSu=50LIo<=mi8Q_H#&xbmD9p8G~@KZ#DK{kjmH?lz#P zRSIJ=O*lr%ps0`0eKGP(b-Xs<~tV@w_)!^aUek7j>y|_8eC3Hfp#6kBXwpdLCG3MeY+b&)ZBp;5(`|)~_ z{3eKLc5SX+XT6FQ0z@|=O&J_0Wq009lSteB95E<)VzIII3LuN%OwRG}ybuwZtO(D@ z+aMZAI$2};Zh4?j`AI0?e)5<7=hG<7{r&^Q#eub!+&2K%O#Y~{BY*wlHZ?_Pk^r+ zBuWxEu9OM$zUY*L+lkVT6v6;oaq=Bd(FT9Z(Pya$Fa>1yafq~k%+TMS{j5Um);BRGR|3=zuez6gcwiEtaNajW06m6#!dF^3v8ddyCK{ z`DK{?Kg5SXy+L%7)OyTc>1EhO3hvXlvR>t7jQ9M?QA>AV`$n=qnsPI-z=eksBcRqK zSX?2VDkZ4rs}0&6c%|yTS(-GU@Ie0IowYnLo49xm+pE7|&yz14yS+m{DWYUy<={`- z!m)_NNkqUGhCTH7V{`f${i!i!P0oLtt0+>yx5NB?7wxMU{j+qZ0CywKH5cs@>?GZQ ze14f3aIwgh3XYEmTKHucV7SOt92`&e1oTT`Xg$9bUJSXX7L zcdpLpOebM}kD5IBtS6#i>A-t!DgGklb`7lL#rq7L*bx$Ojh zw25&sYhgb_0J`fYcB+kdWg<5ch^HGuXxc16_fJXD0sniwp*g;&xthzc#D-mOqti_b zy9hMV#OPJABgb88XM`LlMOpRO*Ci!X)bK>O3ab&Tj}WsAk{cF6&wLS(qwWneOa z37gU!Yl~-TukIoD#VYQTu`obzT_^j}52R$pUl(-9kesV!GXxCFzkVN(Kz4q$c$#9BRx}0wSbJ!5<0pdbA}5TT(o#| zGyd_q*2h)x>GhANiwZ)!2)kAQlXN|JRD`_;Kq zfAgQY=}IF=5k-j_{*=k}k3bxd8E*)+143*z?!kKSI0;YKKS@r1Rn_L4RG$1wa0ij= z1RfRvO|=NI>3wh8l~f=?X_`QKciGmdcs~)`6Z2fOBrK>qDi;E0nPx~E!B5L{d_Za?9zx@Np%~Lt`M91pp$PWTXKU$O^+7yjYass z`$LgjH2FJhnBkOb;ikTs+=i0I!dB~?EFY<6^V9?qR^jkJ$YGcxR@ku zi)|tGUTEUc4fRAT+jKRv*!n(6$%3fST@-gG3vY$l@b_BTl&a48=J(dw9hG1UoxBW* z2@SFyuuc+b+9VSmVx7$)tl0U&GrcC0F@U*L!-OFkx&vZ=X?ObiNH1-`ODfjC*s;gw z%>mZdXgH#$HD!$ftX>e2ye9&amQ?K$zb7J^E+X^$&_i4nd>p#6xDS@D*n=SjbxHHl z5z9?>-_&wmp;m^b#-SH|Ou6-nvj%E$*GLAv?Zq-qrOh%=pR^9hwPxjnVX_p*#dXgN z%aq%Osk|PmRoCBlqVyakbMC_naQY!_U7Jm{*to&Jb9Z7O4-9LV^NW*P^4p)*J(2b7 zd?lTIG^}C0icXx^Z2n-ay)r%cUvD<`%oP(uwYgo%?x6D59#%o5(g?=~lB@x4V@$Sz z2za}eT!r@yO||ChK7&5eBH@z_o@KBhe#2b!$_XW;Y;ag$oc_x0{7{V6_Eo)uA(9#i z!T|>&5`@#VE(fg`YAD_Rm@kaXgg!u}tW3I=?StDEYP-4$OHe%?J^6U(N3L+HhEG+K zIkEn@5d4bNJdSQlHn^G|?KY#d1CR{rt-2X_{e ABLDyZ literal 0 HcmV?d00001 From 60cd907d3a5702216d95b9199b5e12a217859c13 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 28 Oct 2026 16:02:29 +0800 Subject: [PATCH 04/10] fix: discover and invoke AIHubMix decision models --- README.md | 2 +- README_EN.md | 2 +- api-docs/openapi/v3_0/aiFoundationApis.json | 10 +- .../aihubmix/AiHubMixDecisionClient.java | 13 ++ .../provider/aihubmix/AiHubMixProvider.java | 43 +++- .../systemone/SystemOneDecisionClient.java | 169 ++++++++++++++ .../provider/support/AdapterType.java | 3 +- .../typesafe/TypeSafeDecisionClient.java | 155 +------------ .../ProviderTypeConsoleEndpointTest.java | 16 ++ .../aihubmix/AiHubMixDecisionClientTest.java | 206 ++++++++++++++++++ .../aihubmix/AiHubMixProviderTest.java | 2 +- dev/en/sdk-core/decision-models.md | 19 +- dev/zh-CN/sdk-core/decision-models.md | 19 +- .../api/generated/models/adapter-type-info.ts | 3 +- ui/src/api/generated/models/ai-model-spec.ts | 3 +- .../generated/models/discovered-model-item.ts | 3 +- .../models/parameter-mapping-template-info.ts | 3 +- .../generated/models/provider-type-info.ts | 3 +- 18 files changed, 500 insertions(+), 174 deletions(-) create mode 100644 app/src/main/java/run/halo/aifoundation/provider/aihubmix/AiHubMixDecisionClient.java create mode 100644 app/src/main/java/run/halo/aifoundation/provider/protocol/systemone/SystemOneDecisionClient.java create mode 100644 app/src/test/java/run/halo/aifoundation/provider/aihubmix/AiHubMixDecisionClientTest.java diff --git a/README.md b/README.md index c799009e..fb2b3402 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Halo 官方 AI 能力平台,统一接入主流大模型,为插件生态提 ## 功能特性 - **多提供商支持**:内置 OpenAI、OpenRouter、DeepSeek、月之暗面 Kimi、硅基流动、阿里云百炼、豆包、文心一言、智谱开放平台、Ollama、OpenAI 兼容、AIHubMix、Gitee 模力方舟、MiniMax、腾讯云 TokenHub 和 Xiaomi MiMo -- **决策模型**:提供供应商中立的概率判断、单选、评分 API 和决策工作台,内置 TypeSafe / Jev 适配器 +- **决策模型**:提供供应商中立的概率判断、单选、评分 API 和决策工作台,支持通过 TypeSafe 或 AIHubMix 使用 Jev - **统一模型管理**:在 Halo 控制台管理 Provider、API Key Secret、语言 / Embedding / Rerank / 图像生成模型、Adapter 与模型能力 - **模型自动发现**:从 Provider 拉取模型,并保留远程声明、内建能力和管理员覆盖的来源信息 - **统一参数映射**:按 Provider 或模型配置采样、重试、推理、Embedding、Rerank 和图像参数到供应方字段的映射 diff --git a/README_EN.md b/README_EN.md index e6fe4648..5ea97b87 100644 --- a/README_EN.md +++ b/README_EN.md @@ -10,7 +10,7 @@ ecosystem. ## Features -- **Decision models**: Provider-neutral probability, choice, and score APIs with a decision workbench and a built-in TypeSafe / Jev adapter. +- **Decision models**: Provider-neutral probability, choice, and score APIs with a decision workbench and Jev access through TypeSafe or AIHubMix. - **Multiple AI providers**: Built-in support for OpenAI, OpenRouter, DeepSeek, Moonshot Kimi, SiliconFlow, Alibaba Cloud Model Studio, Doubao, ERNIE, Zhipu AI, Ollama, OpenAI-compatible providers, AIHubMix, Gitee AI, MiniMax, Tencent Cloud TokenHub, and Xiaomi MiMo diff --git a/api-docs/openapi/v3_0/aiFoundationApis.json b/api-docs/openapi/v3_0/aiFoundationApis.json index f0ce57c8..3f93bd6c 100644 --- a/api-docs/openapi/v3_0/aiFoundationApis.json +++ b/api-docs/openapi/v3_0/aiFoundationApis.json @@ -1736,7 +1736,7 @@ "properties" : { "adapterType" : { "type" : "string", - "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one" ] + "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one", "aihubmix-system-one" ] }, "defaultParameterMappingOverrides" : { "type" : "object", @@ -1861,7 +1861,7 @@ "adapterType" : { "type" : "string", "description" : "Internal invocation adapter", - "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one" ] + "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one", "aihubmix-system-one" ] }, "capabilities" : { "$ref" : "#/components/schemas/ModelCapabilities" @@ -2317,7 +2317,7 @@ "adapterType" : { "type" : "string", "nullable" : true, - "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one" ] + "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one", "aihubmix-system-one" ] }, "capabilities" : { "$ref" : "#/components/schemas/ModelCapabilities" @@ -3111,7 +3111,7 @@ "type" : "array", "items" : { "type" : "string", - "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one" ] + "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one", "aihubmix-system-one" ] } }, "configurationType" : { @@ -3235,7 +3235,7 @@ "type" : "array", "items" : { "type" : "string", - "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one" ] + "enum" : [ "openai-chat", "openai-responses", "openai-embedding", "openai-image", "deepseek-chat", "deepseek-responses", "deepseek-messages", "dashscope-chat", "dashscope-responses", "dashscope-messages", "dashscope-embedding", "dashscope-compatible-rerank", "dashscope-native-rerank", "doubao-responses", "doubao-chat", "doubao-embedding", "ernie-responses", "ernie-chat", "ernie-messages", "ernie-embedding", "gitee-responses", "gitee-chat", "gitee-messages", "gitee-embedding", "tokenhub-chat", "tokenhub-embedding", "kimi-chat", "minimax-messages", "minimax-chat", "minimax-responses", "openrouter-chat", "openrouter-responses", "openrouter-messages", "openrouter-embedding", "siliconflow-chat", "siliconflow-messages", "siliconflow-embedding", "mimo-responses", "mimo-chat", "mimo-messages", "zhipu-chat", "zhipu-messages", "zhipu-embedding", "aihubmix-responses", "aihubmix-chat", "aihubmix-messages", "aihubmix-embedding", "openrouter-image", "dashscope-image", "doubao-image", "ernie-image", "gitee-image", "minimax-image", "siliconflow-image", "zhipu-image", "aihubmix-image", "anthropic-messages", "gemini-generate-content", "gemini-embed-content", "rerank", "ollama-chat", "ollama-openai-chat", "ollama-responses", "ollama-messages", "ollama-embedding", "ollama-image", "typesafe-system-one", "aihubmix-system-one" ] } }, "supportedFeatures" : { diff --git a/app/src/main/java/run/halo/aifoundation/provider/aihubmix/AiHubMixDecisionClient.java b/app/src/main/java/run/halo/aifoundation/provider/aihubmix/AiHubMixDecisionClient.java new file mode 100644 index 00000000..5cab0b08 --- /dev/null +++ b/app/src/main/java/run/halo/aifoundation/provider/aihubmix/AiHubMixDecisionClient.java @@ -0,0 +1,13 @@ +package run.halo.aifoundation.provider.aihubmix; + +import org.springframework.web.reactive.function.client.WebClient; +import run.halo.aifoundation.provider.protocol.systemone.SystemOneDecisionClient; + +/** AIHubMix System One gateway with its attribution headers and diagnostics. */ +public class AiHubMixDecisionClient extends SystemOneDecisionClient { + + public AiHubMixDecisionClient(String baseUrl, String apiKey, String model, + WebClient.Builder builder) { + super("aihubmix", baseUrl, apiKey, model, AiHubMixHeaders.DEFAULTS, builder); + } +} diff --git a/app/src/main/java/run/halo/aifoundation/provider/aihubmix/AiHubMixProvider.java b/app/src/main/java/run/halo/aifoundation/provider/aihubmix/AiHubMixProvider.java index f6c336cc..12dd94b8 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/aihubmix/AiHubMixProvider.java +++ b/app/src/main/java/run/halo/aifoundation/provider/aihubmix/AiHubMixProvider.java @@ -14,11 +14,13 @@ import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import run.halo.aifoundation.capability.CapabilitySource; +import run.halo.aifoundation.capability.DecisionCapability; import run.halo.aifoundation.capability.ImageGenerationCapability; import run.halo.aifoundation.capability.InputSource; import run.halo.aifoundation.capability.LanguageCapability; import run.halo.aifoundation.capability.ModelCapabilities; import run.halo.aifoundation.capability.ModelCapabilitySources; +import run.halo.aifoundation.decision.DecisionQuestionType; import run.halo.aifoundation.extension.AiProvider; import run.halo.aifoundation.provider.AbstractAiProviderType; import run.halo.aifoundation.provider.mapping.DefaultParameterMapping; @@ -33,6 +35,7 @@ import run.halo.aifoundation.provider.support.ModelFeature; import run.halo.aifoundation.provider.support.ModelType; import run.halo.aifoundation.provider.support.ProviderFeatureSets; +import run.halo.aifoundation.provider.support.ProviderDecisionClient; import run.halo.aifoundation.provider.support.ProviderImageGenerationClient; import run.halo.aifoundation.provider.support.ProviderModelRef; import run.halo.aifoundation.provider.support.ProviderRerankingClient; @@ -61,7 +64,7 @@ public String getDisplayName() { @Override public String getDescription() { - return "AIHubMix 原生网关适配,支持 Responses、Chat、嵌入、重排与模型路由图片预测。"; + return "AIHubMix 原生网关适配,支持 Responses、Chat、嵌入、重排、图片预测与决策模型。"; } @Override @@ -98,7 +101,7 @@ public String getDefaultBaseUrl() { public List getSupportedAdapterTypes() { return List.of(AdapterType.AIHUBMIX_RESPONSES, AdapterType.AIHUBMIX_CHAT, AdapterType.AIHUBMIX_MESSAGES, AdapterType.AIHUBMIX_EMBEDDING, - AdapterType.RERANK, AdapterType.AIHUBMIX_IMAGE); + AdapterType.RERANK, AdapterType.AIHUBMIX_IMAGE, AdapterType.AIHUBMIX_SYSTEM_ONE); } @Override @@ -178,6 +181,39 @@ public ProviderImageGenerationClient buildImageGenerationClient(AiProvider provi AiHubMixHeaders.DEFAULTS), imageWebClientBuilder(provider)); } + @Override + public DecisionCapability decisionCapabilities() { + return DecisionCapability.builder() + .questionTypes(List.of(DecisionQuestionType.YES_NO, DecisionQuestionType.CHOICE, + DecisionQuestionType.SCORE)) + .structuredState(true).maxChoiceOptions(255).maxScoreLevels(10) + .choiceProbabilities(true).choiceConfidence(true) + .scoreProbabilities(true).scoreConfidence(true).scoreLegend(true) + .build(); + } + + @Override + public ProviderDecisionClient buildDecisionClient(AiProvider provider, String apiKey, + ProviderModelRef model) { + var resolved = resolveModel(model); + if (resolved.adapterType() != AdapterType.AIHUBMIX_SYSTEM_ONE) { + throw new IllegalArgumentException( + "Unsupported AIHubMix decision adapter: " + resolved.adapterType()); + } + validateNativeModelOptions(resolved); + return new AiHubMixDecisionClient(resolveBaseUrl(provider), apiKey, resolved.modelId(), + webClientBuilder(provider)); + } + + @Override + public void validateNativeModelOptions(ProviderModelRef model) { + super.validateNativeModelOptions(model); + if (model.modelType() == ModelType.DECISION + && model.nativeOptions() != null && !model.nativeOptions().isEmpty()) { + throw new IllegalArgumentException("AIHubMix decision models do not accept native options"); + } + } + @Override public LanguageModelProviderOptions languageModelProviderOptions() { return languageModelProviderOptions(StructuredOutputSupport.JSON_SCHEMA, true); @@ -264,6 +300,9 @@ private DiscoveredModel toDiscoveredModel(Map node) { if (modelId.isBlank()) { return null; } + if (type(node, "decision")) { + return typedModel(modelId, ModelType.DECISION, AdapterType.AIHUBMIX_SYSTEM_ONE); + } if (type(node, "embedding")) { return typedModel(modelId, ModelType.EMBEDDING, AdapterType.AIHUBMIX_EMBEDDING); } diff --git a/app/src/main/java/run/halo/aifoundation/provider/protocol/systemone/SystemOneDecisionClient.java b/app/src/main/java/run/halo/aifoundation/provider/protocol/systemone/SystemOneDecisionClient.java new file mode 100644 index 00000000..e52a5fa9 --- /dev/null +++ b/app/src/main/java/run/halo/aifoundation/provider/protocol/systemone/SystemOneDecisionClient.java @@ -0,0 +1,169 @@ +package run.halo.aifoundation.provider.protocol.systemone; + +import java.net.URI; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import run.halo.aifoundation.decision.DecisionAnswer; +import run.halo.aifoundation.decision.DecisionQuestion; +import run.halo.aifoundation.decision.DecisionQuestionType; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.decision.DecisionResponse; +import run.halo.aifoundation.decision.DecisionUsage; +import run.halo.aifoundation.provider.support.ProviderDecisionClient; +import run.halo.aifoundation.provider.support.ProviderUris; +import run.halo.aifoundation.provider.transport.ProviderDiagnostics; +import run.halo.aifoundation.provider.transport.ProviderHttpResponseSupport; + +/** Shared System One wire protocol for direct providers and gateways. */ +public class SystemOneDecisionClient implements ProviderDecisionClient { + private static final ParameterizedTypeReference> JSON = + new ParameterizedTypeReference<>() { }; + + private final String providerType; + private final URI endpoint; + private final Map headers; + private final String apiKey; + private final String model; + private final WebClient client; + + public SystemOneDecisionClient(String providerType, String baseUrl, String apiKey, String model, + Map headers, WebClient.Builder builder) { + this.providerType = providerType; + this.endpoint = URI.create(ProviderUris.withoutTrailingPath(baseUrl, "/v1") + "/v1/systemone"); + this.headers = Map.copyOf(headers); + this.apiKey = apiKey; + this.model = model; + this.client = builder.build(); + } + + @Override + public Mono decide(DecisionRequest request) { + return Mono.defer(() -> { + var questions = new LinkedHashMap(); + request.getQuestions().forEach((id, question) -> questions.put(id, encode(question))); + var body = Map.of("model", model, "state", request.getState(), "questions", questions); + var diagnostics = ProviderDiagnostics.create(providerType, "decision"); + diagnostics.request(endpoint.toString(), body, false); + return client.post().uri(endpoint).headers(requestHeaders -> { + headers.forEach(requestHeaders::set); + requestHeaders.setBearerAuth(apiKey); + }) + .bodyValue(body).exchangeToMono(response -> { + if (!response.statusCode().is2xxSuccessful()) { + return ProviderHttpResponseSupport.errorMono(response, providerType, + "decision", diagnostics); + } + diagnostics.responseStatus(response.statusCode().value()); + return response.bodyToMono(JSON).map(SystemOneDecisionClient::decode); + }); + }); + } + + private static Map encode(DecisionQuestion question) { + var body = new LinkedHashMap(); + body.put("instructions", question.getInstructions()); + switch (question.getType()) { + case YES_NO -> { + body.put("type", "noul"); + var criteria = new LinkedHashMap(); + if (question.getYesCriteria() != null) { + criteria.put("true", question.getYesCriteria()); + } + if (question.getNoCriteria() != null) { + criteria.put("false", question.getNoCriteria()); + } + if (!criteria.isEmpty()) { + body.put("criteria", criteria); + } + } + case CHOICE -> { + if (question.getOptions().size() > 255) { + throw new IllegalArgumentException("System One supports at most 255 choice options"); + } + body.put("type", "choice"); + body.put("criteria", question.getOptions()); + } + case SCORE -> { + if (question.getLevels().size() > 10) { + throw new IllegalArgumentException("System One supports at most 10 score levels"); + } + body.put("type", "score"); + body.put("criteria", question.getLevels()); + } + } + return body; + } + + private static DecisionResponse decode(Map body) { + var answers = new LinkedHashMap(); + object(body.get("answers")).forEach((id, raw) -> answers.put(id, answer(object(raw)))); + DecisionUsage usage = null; + if (body.get("usage") != null) { + var raw = object(body.get("usage")); + usage = new DecisionUsage(tokens(raw.get("input_tokens")), tokens(raw.get("output_tokens"))); + } + return new DecisionResponse(string(body.get("model")), answers, usage); + } + + private static DecisionAnswer answer(Map raw) { + var answer = DecisionAnswer.builder(); + switch (string(raw.get("type"))) { + case "noul" -> answer.type(DecisionQuestionType.YES_NO).probability(number(raw.get("noul"))); + case "choice" -> answer.type(DecisionQuestionType.CHOICE) + .choice(string(raw.get("choice"))) + .probabilities(probabilities(raw.get("probabilities"))) + .confidence(number(raw.get("confidence"))); + case "score" -> { + var legend = new LinkedHashMap(); + object(raw.get("legend")).forEach((key, value) -> legend.put(key, string(value))); + answer.type(DecisionQuestionType.SCORE).score(number(raw.get("score"))) + .probabilities(probabilities(raw.get("probabilities"))) + .confidence(number(raw.get("confidence"))).legend(legend); + } + default -> throw new IllegalStateException("Unsupported System One answer type"); + } + return answer.build(); + } + + private static Map probabilities(Object raw) { + var result = new LinkedHashMap(); + object(raw).forEach((key, value) -> result.put(key, number(value))); + return result; + } + + @SuppressWarnings("unchecked") + private static Map object(Object raw) { + if (!(raw instanceof Map map) || map.keySet().stream().anyMatch(k -> !(k instanceof String))) { + throw new IllegalStateException("Expected an object in System One response"); + } + return (Map) map; + } + + private static String string(Object raw) { + if (!(raw instanceof String value) || value.isBlank()) { + throw new IllegalStateException("Expected a non-empty string in System One response"); + } + return value; + } + + private static Double number(Object raw) { + if (!(raw instanceof Number value) || !Double.isFinite(value.doubleValue())) { + throw new IllegalStateException("Expected a finite number in System One response"); + } + return value.doubleValue(); + } + + private static Integer tokens(Object raw) { + if (raw == null) { + return null; + } + var count = number(raw); + if (count < 0 || count > Integer.MAX_VALUE || count != Math.rint(count)) { + throw new IllegalStateException("Invalid token count in System One response"); + } + return count.intValue(); + } +} diff --git a/app/src/main/java/run/halo/aifoundation/provider/support/AdapterType.java b/app/src/main/java/run/halo/aifoundation/provider/support/AdapterType.java index 33885424..07133574 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/support/AdapterType.java +++ b/app/src/main/java/run/halo/aifoundation/provider/support/AdapterType.java @@ -93,7 +93,8 @@ public enum AdapterType { OLLAMA_MESSAGES("ollama-messages", ModelType.LANGUAGE, AdapterProtocol.MESSAGES), OLLAMA_EMBEDDING("ollama-embedding", ModelType.EMBEDDING, AdapterProtocol.EMBEDDING), OLLAMA_IMAGE("ollama-image", ModelType.IMAGE_GENERATION, AdapterProtocol.IMAGE_GENERATION), - TYPESAFE_SYSTEM_ONE("typesafe-system-one", ModelType.DECISION, AdapterProtocol.SYSTEM_ONE); + TYPESAFE_SYSTEM_ONE("typesafe-system-one", ModelType.DECISION, AdapterProtocol.SYSTEM_ONE), + AIHUBMIX_SYSTEM_ONE("aihubmix-system-one", ModelType.DECISION, AdapterProtocol.SYSTEM_ONE); private final String value; private final ModelType modelType; diff --git a/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClient.java b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClient.java index ebd555de..3bee4c3c 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClient.java +++ b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClient.java @@ -1,161 +1,14 @@ package run.halo.aifoundation.provider.typesafe; -import java.net.URI; -import java.util.LinkedHashMap; import java.util.Map; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; -import run.halo.aifoundation.decision.DecisionAnswer; -import run.halo.aifoundation.decision.DecisionQuestion; -import run.halo.aifoundation.decision.DecisionQuestionType; -import run.halo.aifoundation.decision.DecisionRequest; -import run.halo.aifoundation.decision.DecisionResponse; -import run.halo.aifoundation.decision.DecisionUsage; -import run.halo.aifoundation.provider.support.ProviderDecisionClient; -import run.halo.aifoundation.provider.transport.ProviderDiagnostics; -import run.halo.aifoundation.provider.transport.ProviderHttpResponseSupport; +import run.halo.aifoundation.provider.protocol.systemone.SystemOneDecisionClient; -/** TypeSafe System One wire protocol; vendor names do not leak into the public SDK. */ -public class TypeSafeDecisionClient implements ProviderDecisionClient { - private static final ParameterizedTypeReference> JSON = - new ParameterizedTypeReference<>() { }; - - private final URI endpoint; - private final String apiKey; - private final String model; - private final WebClient client; +/** Direct TypeSafe access using the System One protocol. */ +public class TypeSafeDecisionClient extends SystemOneDecisionClient { public TypeSafeDecisionClient(String baseUrl, String apiKey, String model, WebClient.Builder builder) { - this.endpoint = URI.create(baseUrl.replaceAll("/+$", "") + "/v1/systemone"); - this.apiKey = apiKey; - this.model = model; - this.client = builder.build(); - } - - @Override - public Mono decide(DecisionRequest request) { - return Mono.defer(() -> { - var questions = new LinkedHashMap(); - request.getQuestions().forEach((id, question) -> questions.put(id, encode(question))); - var body = Map.of("model", model, "state", request.getState(), "questions", questions); - var diagnostics = ProviderDiagnostics.create("typesafe", "decision"); - diagnostics.request(endpoint.toString(), body, false); - return client.post().uri(endpoint).headers(headers -> headers.setBearerAuth(apiKey)) - .bodyValue(body).exchangeToMono(response -> { - if (!response.statusCode().is2xxSuccessful()) { - return ProviderHttpResponseSupport.errorMono(response, "typesafe", - "decision", diagnostics); - } - diagnostics.responseStatus(response.statusCode().value()); - return response.bodyToMono(JSON).map(TypeSafeDecisionClient::decode); - }); - }); - } - - private static Map encode(DecisionQuestion question) { - var body = new LinkedHashMap(); - body.put("instructions", question.getInstructions()); - switch (question.getType()) { - case YES_NO -> { - body.put("type", "noul"); - var criteria = new LinkedHashMap(); - if (question.getYesCriteria() != null) { - criteria.put("true", question.getYesCriteria()); - } - if (question.getNoCriteria() != null) { - criteria.put("false", question.getNoCriteria()); - } - if (!criteria.isEmpty()) { - body.put("criteria", criteria); - } - } - case CHOICE -> { - if (question.getOptions().size() > 255) { - throw new IllegalArgumentException("TypeSafe supports at most 255 choice options"); - } - body.put("type", "choice"); - body.put("criteria", question.getOptions()); - } - case SCORE -> { - if (question.getLevels().size() > 10) { - throw new IllegalArgumentException("TypeSafe supports at most 10 score levels"); - } - body.put("type", "score"); - body.put("criteria", question.getLevels()); - } - } - return body; - } - - private static DecisionResponse decode(Map body) { - var answers = new LinkedHashMap(); - object(body.get("answers")).forEach((id, raw) -> answers.put(id, answer(object(raw)))); - DecisionUsage usage = null; - if (body.get("usage") != null) { - var raw = object(body.get("usage")); - usage = new DecisionUsage(tokens(raw.get("input_tokens")), tokens(raw.get("output_tokens"))); - } - return new DecisionResponse(string(body.get("model")), answers, usage); - } - - private static DecisionAnswer answer(Map raw) { - var answer = DecisionAnswer.builder(); - switch (string(raw.get("type"))) { - case "noul" -> answer.type(DecisionQuestionType.YES_NO).probability(number(raw.get("noul"))); - case "choice" -> answer.type(DecisionQuestionType.CHOICE) - .choice(string(raw.get("choice"))) - .probabilities(probabilities(raw.get("probabilities"))) - .confidence(number(raw.get("confidence"))); - case "score" -> { - var legend = new LinkedHashMap(); - object(raw.get("legend")).forEach((key, value) -> legend.put(key, string(value))); - answer.type(DecisionQuestionType.SCORE).score(number(raw.get("score"))) - .probabilities(probabilities(raw.get("probabilities"))) - .confidence(number(raw.get("confidence"))).legend(legend); - } - default -> throw new IllegalStateException("Unsupported TypeSafe answer type"); - } - return answer.build(); - } - - private static Map probabilities(Object raw) { - var result = new LinkedHashMap(); - object(raw).forEach((key, value) -> result.put(key, number(value))); - return result; - } - - @SuppressWarnings("unchecked") - private static Map object(Object raw) { - if (!(raw instanceof Map map) || map.keySet().stream().anyMatch(k -> !(k instanceof String))) { - throw new IllegalStateException("Expected an object in TypeSafe response"); - } - return (Map) map; - } - - private static String string(Object raw) { - if (!(raw instanceof String value) || value.isBlank()) { - throw new IllegalStateException("Expected a non-empty string in TypeSafe response"); - } - return value; - } - - private static Double number(Object raw) { - if (!(raw instanceof Number value) || !Double.isFinite(value.doubleValue())) { - throw new IllegalStateException("Expected a finite number in TypeSafe response"); - } - return value.doubleValue(); - } - - private static Integer tokens(Object raw) { - if (raw == null) { - return null; - } - var count = number(raw); - if (count < 0 || count > Integer.MAX_VALUE || count != Math.rint(count)) { - throw new IllegalStateException("Invalid token count in TypeSafe response"); - } - return count.intValue(); + super("typesafe", baseUrl, apiKey, model, Map.of(), builder); } } diff --git a/app/src/test/java/run/halo/aifoundation/endpoint/ProviderTypeConsoleEndpointTest.java b/app/src/test/java/run/halo/aifoundation/endpoint/ProviderTypeConsoleEndpointTest.java index 20083036..16fad089 100644 --- a/app/src/test/java/run/halo/aifoundation/endpoint/ProviderTypeConsoleEndpointTest.java +++ b/app/src/test/java/run/halo/aifoundation/endpoint/ProviderTypeConsoleEndpointTest.java @@ -32,6 +32,22 @@ class ProviderTypeConsoleEndpointTest { .configureClient() .build(); + @Test + void listProviderTypesExposesAiHubMixDecisionAdapter() { + when(providerClientCache.getProviderTypeMap()) + .thenReturn(Map.of("aihubmix", new AiHubMixProvider())); + + webTestClient.get().uri("/provider-types") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$[0].supportedModelTypes[4]").isEqualTo("decision") + .jsonPath("$[0].adapters[?(@.adapterType == 'aihubmix-system-one')].modelType") + .isEqualTo("decision") + .jsonPath("$[0].adapters[?(@.adapterType == 'aihubmix-system-one')].displayName") + .isEqualTo("System One"); + } + @Test void listProviderTypes_includesXiaomiMiMoMetadata() { when(providerClientCache.getProviderTypeMap()) diff --git a/app/src/test/java/run/halo/aifoundation/provider/aihubmix/AiHubMixDecisionClientTest.java b/app/src/test/java/run/halo/aifoundation/provider/aihubmix/AiHubMixDecisionClientTest.java new file mode 100644 index 00000000..4fd85383 --- /dev/null +++ b/app/src/test/java/run/halo/aifoundation/provider/aihubmix/AiHubMixDecisionClientTest.java @@ -0,0 +1,206 @@ +package run.halo.aifoundation.provider.aihubmix; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.sun.net.httpserver.HttpServer; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import reactor.test.StepVerifier; +import run.halo.aifoundation.decision.DecisionQuestion; +import run.halo.aifoundation.decision.DecisionRequest; +import run.halo.aifoundation.extension.AiProvider; +import run.halo.aifoundation.provider.contract.ProviderContractSource; +import run.halo.aifoundation.provider.support.AdapterType; +import run.halo.aifoundation.provider.support.DiscoveryConfidence; +import run.halo.aifoundation.provider.support.DiscoverySource; +import run.halo.aifoundation.provider.support.ModelType; +import run.halo.aifoundation.provider.support.ProviderModelRef; +import run.halo.aifoundation.provider.transport.ProviderHttpException; +import run.halo.aifoundation.service.decision.DecisionModelImpl; +import run.halo.aifoundation.service.observation.UsageExecutionObserver; +import tools.jackson.databind.json.JsonMapper; + +@ProviderContractSource( + provider = "aihubmix", + officialDocumentation = "https://aihubmix.com/model/jev-latest/llms.txt; " + + "https://aihubmix.com/api/v1/models; " + + "https://docs.aihubmix.com/cn/api/App-code", + retrievedAt = "2026-10-28" +) +class AiHubMixDecisionClientTest { + private final AiHubMixProvider providerType = new AiHubMixProvider(); + private final AtomicReference response = new AtomicReference<>("{}"); + private final AtomicReference requestBody = new AtomicReference<>(); + private final AtomicReference path = new AtomicReference<>(); + private final AtomicReference authorization = new AtomicReference<>(); + private final AtomicReference appCode = new AtomicReference<>(); + private final AtomicInteger status = new AtomicInteger(200); + private HttpServer server; + private String baseUrl; + + @BeforeEach + void start() throws Exception { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + path.set(exchange.getRequestURI().getPath()); + authorization.set(exchange.getRequestHeaders().getFirst("Authorization")); + appCode.set(exchange.getRequestHeaders().getFirst("APP-Code")); + requestBody.set(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)); + var bytes = response.get().getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(status.get(), bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + }); + server.start(); + baseUrl = "http://127.0.0.1:" + server.getAddress().getPort(); + } + + @AfterEach + void stop() { + server.stop(0); + } + + @Test + void discoversAndCallsDecisionModelsFromCatalogTypeWithoutModelNameInference() { + // Jev fields match the public catalog retrieved on 2026-10-28. + response.set(""" + {"data":[ + {"model_id":"jev-latest","types":"decision","vendor":"typesafe","endpoints":""}, + {"model_id":"jev-1.13","types":"decision","vendor":"typesafe","endpoints":""}, + {"model_id":"jev-1.14","types":"decision","vendor":"typesafe","endpoints":""}, + {"model_id":"future-judge","type":["decision"]}, + {"model_id":"jev-chat","types":"llm"}, + {"model_id":"voice-model","types":"tts"}],"success":true} + """); + var provider = provider("/v1/"); + var models = providerType.discoverModels(provider, "test-key").block(); + assertThat(path.get()).isEqualTo("/api/v1/models"); + assertThat(authorization.get()).isEqualTo("Bearer test-key"); + assertThat(appCode.get()).isEqualTo("NEUE3459"); + assertThat(models).hasSize(5); + assertThat(models.getLast().modelType()).isEqualTo(ModelType.LANGUAGE); + var decisions = models.stream().filter(model -> model.modelType() == ModelType.DECISION) + .toList(); + assertThat(decisions).extracting(model -> model.modelId()) + .containsExactly("jev-latest", "jev-1.13", "jev-1.14", "future-judge"); + for (var model : decisions) { + assertThat(model.adapterType()).isEqualTo(AdapterType.AIHUBMIX_SYSTEM_ONE); + assertThat(model.source()).isEqualTo(DiscoverySource.REMOTE); + assertThat(model.confidence()).isEqualTo(DiscoveryConfidence.HIGH); + response.set(""" + {"model":"served-version","answers":{"urgent":{"type":"noul","noul":0.9}}} + """); + var client = providerType.buildDecisionClient(provider, "test-key", new ProviderModelRef( + model.modelId(), model.modelType(), model.adapterType())); + StepVerifier.create(client.decide(yesNoRequest())).assertNext(result -> { + assertThat(result.getAnswers().get("urgent").getProbability()).isEqualTo(0.9); + assertThat(result.getUsage()).isNull(); + }).verifyComplete(); + assertThat(path.get()).isEqualTo("/v1/systemone"); + assertThat(JsonMapper.builder().build().readTree(requestBody.get()).path("model").asText()) + .isEqualTo(model.modelId()); + } + } + + @ParameterizedTest + @CsvSource({"'', /v1/systemone", "/, /v1/systemone", "/v1, /v1/systemone", + "/v1/, /v1/systemone", "/gateway/v1/, /gateway/v1/systemone"}) + void evaluatesMixedQuestionsWithGatewayHeadersAndNormalizedBaseUrl(String suffix, String endpoint) { + response.set(""" + {"model":"jev-1.14.0","answers":{ + "urgent":{"type":"noul","noul":0.9}, + "route":{"type":"choice","choice":"review", + "probabilities":{"review":0.8,"publish":0.2},"confidence":0.6}, + "severity":{"type":"score","score":0.25,"legend":{"0":"low","1":"high"}, + "probabilities":{"0":0.75,"1":0.25},"confidence":0.5}}, + "usage":{"input_tokens":123,"output_tokens":22}} + """); + var request = DecisionRequest.builder().state(Map.of("comment", "please review")) + .questions(Map.of("urgent", DecisionQuestion.yesNo("Is this urgent?"), + "route", DecisionQuestion.choice("What next?", + Map.of("review", "Review", "publish", "Publish")), + "severity", DecisionQuestion.score("How severe?", List.of("low", "high")))) + .metadata(Map.of("private", "local-only")).build(); + // A missing adapter resolves to the provider's decision adapter, including manual imports. + var client = providerType.buildDecisionClient(provider(suffix), "test-key", + new ProviderModelRef("jev-latest", ModelType.DECISION, null)); + var runtime = new DecisionModelImpl(client, new UsageExecutionObserver(), + providerType.decisionCapabilities()); + StepVerifier.create(runtime.decide(request)).assertNext(result -> { + assertThat(result.getModel()).isEqualTo("jev-1.14.0"); + assertThat(result.getAnswers().get("urgent").getProbability()).isEqualTo(0.9); + assertThat(result.getAnswers().get("route").getChoice()).isEqualTo("review"); + assertThat(result.getAnswers().get("route").getConfidence()).isEqualTo(0.6); + assertThat(result.getAnswers().get("severity").getScore()).isEqualTo(0.25); + assertThat(result.getAnswers().get("severity").getLegend()).containsEntry("0", "low"); + assertThat(result.getUsage().getInputTokens()).isEqualTo(123); + assertThat(result.getUsage().getOutputTokens()).isEqualTo(22); + }).verifyComplete(); + assertThat(path.get()).isEqualTo(endpoint); + assertThat(authorization.get()).isEqualTo("Bearer test-key"); + assertThat(appCode.get()).isEqualTo("NEUE3459"); + var body = JsonMapper.builder().build().readTree(requestBody.get()); + assertThat(body.path("model").asText()).isEqualTo("jev-latest"); + assertThat(body.path("state").path("comment").asText()).isEqualTo("please review"); + assertThat(body.path("questions").path("urgent").path("type").asText()).isEqualTo("noul"); + assertThat(body.path("questions").path("route").path("criteria").path("review").asText()) + .isEqualTo("Review"); + assertThat(body.path("questions").path("severity").path("criteria").size()).isEqualTo(2); + assertThat(requestBody.get()).doesNotContain("local-only", "metadata", "messages"); + } + + @Test + void attributesGatewayErrorsToAiHubMix() { + status.set(429); + response.set("{\"error\":{\"message\":\"Rate limited\"},\"tid\":\"trace-123\"}"); + var client = providerType.buildDecisionClient(provider("/v1"), "test-key", + new ProviderModelRef("jev-latest", ModelType.DECISION, AdapterType.AIHUBMIX_SYSTEM_ONE)); + StepVerifier.create(client.decide(yesNoRequest())).expectErrorSatisfies(error -> { + assertThat(error).isInstanceOf(ProviderHttpException.class); + var http = (ProviderHttpException) error; + assertThat(http.getProviderType()).isEqualTo("aihubmix"); + assertThat(http.getOperation()).isEqualTo("decision"); + assertThat(http.getStatusCode()).isEqualTo(429); + assertThat(http.getResponseBody()).contains("trace-123"); + }).verify(); + } + + @Test + void rejectsUnsupportedDecisionOptionsWithoutChangingOtherDomains() { + var decision = new ProviderModelRef("jev-latest", ModelType.DECISION, + AdapterType.AIHUBMIX_SYSTEM_ONE, Map.of("temperature", 0.5)); + assertThatThrownBy(() -> providerType.validateNativeModelOptions(decision)) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("native options"); + assertThatThrownBy(() -> providerType.buildDecisionClient(provider("/v1"), "key", decision)) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("native options"); + var language = new ProviderModelRef("chat-model", ModelType.LANGUAGE, + AdapterType.AIHUBMIX_RESPONSES, Map.of("temperature", 0.5)); + providerType.validateNativeModelOptions(language); + assertThatThrownBy(() -> providerType.buildDecisionClient(provider("/v1"), "key", language)) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("decision adapter"); + } + + private AiProvider provider(String suffix) { + var provider = new AiProvider(); + provider.setSpec(new AiProvider.AiProviderSpec()); + provider.getSpec().setProviderType("aihubmix"); + provider.getSpec().setBaseUrl(baseUrl + suffix); + return provider; + } + + private DecisionRequest yesNoRequest() { + return DecisionRequest.builder().state("Please help now") + .questions(Map.of("urgent", DecisionQuestion.yesNo("Is this urgent?"))).build(); + } +} diff --git a/app/src/test/java/run/halo/aifoundation/provider/aihubmix/AiHubMixProviderTest.java b/app/src/test/java/run/halo/aifoundation/provider/aihubmix/AiHubMixProviderTest.java index 319c2856..999984cc 100644 --- a/app/src/test/java/run/halo/aifoundation/provider/aihubmix/AiHubMixProviderTest.java +++ b/app/src/test/java/run/halo/aifoundation/provider/aihubmix/AiHubMixProviderTest.java @@ -77,7 +77,7 @@ void declaresProviderOwnedAdaptersClientsAndResponsesDefault() { assertThat(providerType.getSupportedAdapterTypes()).containsExactly( AdapterType.AIHUBMIX_RESPONSES, AdapterType.AIHUBMIX_CHAT, AdapterType.AIHUBMIX_MESSAGES, AdapterType.AIHUBMIX_EMBEDDING, - AdapterType.RERANK, AdapterType.AIHUBMIX_IMAGE); + AdapterType.RERANK, AdapterType.AIHUBMIX_IMAGE, AdapterType.AIHUBMIX_SYSTEM_ONE); assertThat(providerType.buildChatModel(provider, "key", "gpt-5")) .isInstanceOf(AiHubMixResponsesModel.class); assertThat(providerType.buildChatModel(provider, "key", new ProviderModelRef( diff --git a/dev/en/sdk-core/decision-models.md b/dev/en/sdk-core/decision-models.md index f0592a11..867741db 100644 --- a/dev/en/sdk-core/decision-models.md +++ b/dev/en/sdk-core/decision-models.md @@ -3,8 +3,8 @@ [简体中文](../../zh-CN/sdk-core/decision-models.md) | English Decision models answer closed questions about shared text or JSON state. Consumer plugins own thresholds -and subsequent actions. `DecisionModel` is provider-neutral; TypeSafe / Jev is the first built-in -provider. Other adapters can reuse model resolution, the workbench, and usage recording. +and subsequent actions. `DecisionModel` is provider-neutral, with built-in TypeSafe and AIHubMix +decision adapters. Other adapters can reuse model resolution, the workbench, and usage recording. These APIs are new in the development build and require matching plugin and Java/npm SDK versions; the published `1.0.0` API does not include them. @@ -21,6 +21,18 @@ The adapter supports text/JSON state and `YES_NO`, `CHOICE`, and `SCORE` questio and automatic business routing are outside this adapter's current scope. Unsupported native options are rejected explicitly. +## Use Jev through AIHubMix + +Create an **AIHubMix** provider with its API key and the default Base URL `https://aihubmix.com/v1`. +Refresh model discovery and import a Jev model returned by the catalog, such as `jev-latest`. +Catalog entries with type `decision` map to decision models with adapter `aihubmix-system-one`. +Manual entry of an available model ID is also supported; discovery does not hardcode Jev versions. + +Requests use AIHubMix's `POST /v1/systemone`, bearer authentication, and `APP-Code` header. +Both service-root and `/v1` Base URLs work without duplicating `/v1`. The supported question types +and limits match the System One adapter above. Imported models can run in the Decision workbench +without creating a separate TypeSafe provider. + ## Java ```java @@ -106,4 +118,5 @@ and remove decision resources/default slots before downgrading. Automated tests runtime and workbench; evaluate Chinese/domain-specific accuracy and thresholds with your own data. References: [System One](https://docs.typesafe.ai/api), [model discovery](https://docs.typesafe.ai/api/list-models), -[confidence](https://docs.typesafe.ai/confidence). +[confidence](https://docs.typesafe.ai/confidence), [AIHubMix Jev API](https://aihubmix.com/model/jev-latest/llms.txt), +[AIHubMix model catalog](https://docs.aihubmix.com/cn/api/Models-API). diff --git a/dev/zh-CN/sdk-core/decision-models.md b/dev/zh-CN/sdk-core/decision-models.md index 77537814..cb1a80ed 100644 --- a/dev/zh-CN/sdk-core/decision-models.md +++ b/dev/zh-CN/sdk-core/decision-models.md @@ -3,7 +3,7 @@ 简体中文 | [English](../../en/sdk-core/decision-models.md) 决策模型针对共享内容回答封闭问题,返回概率判断、候选选择或有序评分。业务插件决定阈值和后续动作。 -`DecisionModel` 是供应商中立的公共契约,TypeSafe / Jev 是首个内置提供商;其他提供商可以通过独立 +`DecisionModel` 是供应商中立的公共契约,内置 TypeSafe 与 AIHubMix 决策适配器;其他提供商可以通过独立 适配器接入相同的模型解析、工作台和用量统计。 这些 API 是本次开发新增能力,使用时需要包含此功能的 AI Foundation 插件和 Java/npm SDK;已发布的 `1.0.0` API 不包含它们。 @@ -20,6 +20,16 @@ 评分问题为 2–10 个有序等级。不提供图片/Box、文本生成、工具执行或自动业务路由。 原生参数不支持时会明确拒绝,不会静默忽略。 +## 通过 AIHubMix 使用 Jev + +在提供商管理中选择 **AIHubMix** 并配置它的 API Key,默认 Base URL 为 `https://aihubmix.com/v1`。 +打开模型发现并刷新列表,导入目录返回的 Jev 模型(如 `jev-latest`)。目录中的 `decision` 类型会映射为 +决策模型,适配器为 `aihubmix-system-one`;也支持手动填写目录中可用的模型 ID,不限定 Jev 版本。 + +调用使用 AIHubMix 的 `POST /v1/systemone`,带上 AIHubMix 的鉴权与 `APP-Code` 请求头。 +服务根地址和以 `/v1` 结尾的 Base URL 均可使用;适配器不会重复追加 `/v1`。支持的问题类型和限制与 +上述 System One 适配一致,导入后可直接在“决策”工作台测试,无需另建 TypeSafe 提供商。 + ## Java 调用 ```java @@ -82,7 +92,7 @@ TypeSafe 客户端严格解析;公共层放宽可选字段不会掩盖它的 ## 浏览器调用自己的插件接口 消费插件可以接收 `DecisionRequest`、调用 `AiModelService`,并把 `DecisionResponse` 作为 JSON 返回。 -使用消费插件自己的鉴权和业务校验;浏览器不直接持有 TypeSafe API Key。 +使用消费插件自己的鉴权和业务校验;浏览器不直接持有提供商 API Key。 ```ts import { createDecisionClient } from '@halo-dev/ai-foundation-sdk' @@ -109,10 +119,11 @@ if (answer?.type === 'YES_NO') { 原有语言、嵌入、重排和图像 API 及持久化值保持不变。新服务方法提供默认实现,旧的 `AiModelService` 实现可继续加载;新功能仍要求匹配版本的运行时。 -新增 `decision` 模型类型、`typesafe-system-one` 适配器及默认决策槽位后,旧版插件/生成客户端不能 +新增 `decision` 模型类型、`typesafe-system-one` / `aihubmix-system-one` 适配器及默认决策槽位后,旧版插件/生成客户端不能 识别新枚举。升级时一起更新相关 SDK;降级前移除新模型资源并清空决策默认槽位。 自动测试覆盖协议、模型解析、验证、取消、超时和工作台。实际中文业务效果与阈值应使用自己的数据评估。 协议依据:[System One](https://docs.typesafe.ai/api)、[模型发现](https://docs.typesafe.ai/api/list-models)、 -[概率与置信度](https://docs.typesafe.ai/confidence)。 +[概率与置信度](https://docs.typesafe.ai/confidence)、[AIHubMix Jev 接口](https://aihubmix.com/model/jev-latest/llms.txt)、 +[AIHubMix 模型目录](https://docs.aihubmix.com/cn/api/Models-API)。 diff --git a/ui/src/api/generated/models/adapter-type-info.ts b/ui/src/api/generated/models/adapter-type-info.ts index b975ad51..4404576d 100644 --- a/ui/src/api/generated/models/adapter-type-info.ts +++ b/ui/src/api/generated/models/adapter-type-info.ts @@ -134,7 +134,8 @@ export const AdapterTypeInfoAdapterTypeEnum = { OllamaMessages: 'ollama-messages', OllamaEmbedding: 'ollama-embedding', OllamaImage: 'ollama-image', - TypesafeSystemOne: 'typesafe-system-one' + TypesafeSystemOne: 'typesafe-system-one', + AihubmixSystemOne: 'aihubmix-system-one' } as const; export type AdapterTypeInfoAdapterTypeEnum = typeof AdapterTypeInfoAdapterTypeEnum[keyof typeof AdapterTypeInfoAdapterTypeEnum]; diff --git a/ui/src/api/generated/models/ai-model-spec.ts b/ui/src/api/generated/models/ai-model-spec.ts index 115ed99f..e70d5370 100644 --- a/ui/src/api/generated/models/ai-model-spec.ts +++ b/ui/src/api/generated/models/ai-model-spec.ts @@ -176,7 +176,8 @@ export const AiModelSpecAdapterTypeEnum = { OllamaMessages: 'ollama-messages', OllamaEmbedding: 'ollama-embedding', OllamaImage: 'ollama-image', - TypesafeSystemOne: 'typesafe-system-one' + TypesafeSystemOne: 'typesafe-system-one', + AihubmixSystemOne: 'aihubmix-system-one' } as const; export type AiModelSpecAdapterTypeEnum = typeof AiModelSpecAdapterTypeEnum[keyof typeof AiModelSpecAdapterTypeEnum]; diff --git a/ui/src/api/generated/models/discovered-model-item.ts b/ui/src/api/generated/models/discovered-model-item.ts index af2af06e..11748a4b 100644 --- a/ui/src/api/generated/models/discovered-model-item.ts +++ b/ui/src/api/generated/models/discovered-model-item.ts @@ -155,7 +155,8 @@ export const DiscoveredModelItemAdapterTypeEnum = { OllamaMessages: 'ollama-messages', OllamaEmbedding: 'ollama-embedding', OllamaImage: 'ollama-image', - TypesafeSystemOne: 'typesafe-system-one' + TypesafeSystemOne: 'typesafe-system-one', + AihubmixSystemOne: 'aihubmix-system-one' } as const; export type DiscoveredModelItemAdapterTypeEnum = typeof DiscoveredModelItemAdapterTypeEnum[keyof typeof DiscoveredModelItemAdapterTypeEnum]; diff --git a/ui/src/api/generated/models/parameter-mapping-template-info.ts b/ui/src/api/generated/models/parameter-mapping-template-info.ts index 69c21a4d..1e93ff24 100644 --- a/ui/src/api/generated/models/parameter-mapping-template-info.ts +++ b/ui/src/api/generated/models/parameter-mapping-template-info.ts @@ -146,7 +146,8 @@ export const ParameterMappingTemplateInfoAdapterTypesEnum = { OllamaMessages: 'ollama-messages', OllamaEmbedding: 'ollama-embedding', OllamaImage: 'ollama-image', - TypesafeSystemOne: 'typesafe-system-one' + TypesafeSystemOne: 'typesafe-system-one', + AihubmixSystemOne: 'aihubmix-system-one' } as const; export type ParameterMappingTemplateInfoAdapterTypesEnum = typeof ParameterMappingTemplateInfoAdapterTypesEnum[keyof typeof ParameterMappingTemplateInfoAdapterTypesEnum]; diff --git a/ui/src/api/generated/models/provider-type-info.ts b/ui/src/api/generated/models/provider-type-info.ts index 0dd4699b..28c48ec5 100644 --- a/ui/src/api/generated/models/provider-type-info.ts +++ b/ui/src/api/generated/models/provider-type-info.ts @@ -227,7 +227,8 @@ export const ProviderTypeInfoSupportedAdapterTypesEnum = { OllamaMessages: 'ollama-messages', OllamaEmbedding: 'ollama-embedding', OllamaImage: 'ollama-image', - TypesafeSystemOne: 'typesafe-system-one' + TypesafeSystemOne: 'typesafe-system-one', + AihubmixSystemOne: 'aihubmix-system-one' } as const; export type ProviderTypeInfoSupportedAdapterTypesEnum = typeof ProviderTypeInfoSupportedAdapterTypesEnum[keyof typeof ProviderTypeInfoSupportedAdapterTypesEnum]; From 3e7ed780ce19027796bc81593d292d0e16c5b4c7 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 28 Oct 2026 16:13:17 +0800 Subject: [PATCH 05/10] fix: identify decision workbench caller in usage records --- .../StackWalkingCallerPluginResolver.java | 9 ++-- .../endpoint/DecisionConsoleEndpointTest.java | 54 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/run/halo/aifoundation/service/audit/StackWalkingCallerPluginResolver.java b/app/src/main/java/run/halo/aifoundation/service/audit/StackWalkingCallerPluginResolver.java index 3df803d4..67c5f663 100644 --- a/app/src/main/java/run/halo/aifoundation/service/audit/StackWalkingCallerPluginResolver.java +++ b/app/src/main/java/run/halo/aifoundation/service/audit/StackWalkingCallerPluginResolver.java @@ -20,8 +20,9 @@ public class StackWalkingCallerPluginResolver implements CallerPluginResolver { private static final StackWalker STACK_WALKER = StackWalker.getInstance(RETAIN_CLASS_REFERENCE); private static final String DETECTION_SOURCE = "stack-classloader"; - private static final String CONSOLE_TEST_CLASS_NAME = - "run.halo.aifoundation.endpoint.ModelConsoleEndpoint"; + private static final List CONSOLE_TEST_CLASS_NAMES = List.of( + "run.halo.aifoundation.endpoint.ModelConsoleEndpoint", + "run.halo.aifoundation.endpoint.DecisionConsoleEndpoint"); private static final List FRAMEWORK_PACKAGE_PREFIXES = List.of( "java.", "jdk.", "reactor.", "org.springframework.", "org.pf4j.", "run.halo.aifoundation."); @@ -72,8 +73,8 @@ private Optional detectCallerPlugin() { // Only the Console test boundary may identify Foundation as its own caller. // Include nested classes for deferred RAG reranking invocations. if (type.getClassLoader() == self.getPluginClassLoader() - && (type.getName().equals(CONSOLE_TEST_CLASS_NAME) - || type.getName().startsWith(CONSOLE_TEST_CLASS_NAME + "$"))) { + && CONSOLE_TEST_CLASS_NAMES.stream().anyMatch(name -> + type.getName().equals(name) || type.getName().startsWith(name + "$"))) { return self; } return isCandidateCallerClass(type) diff --git a/app/src/test/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpointTest.java b/app/src/test/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpointTest.java index 667a1443..7cd07a59 100644 --- a/app/src/test/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpointTest.java +++ b/app/src/test/java/run/halo/aifoundation/endpoint/DecisionConsoleEndpointTest.java @@ -7,9 +7,17 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; +import org.pf4j.PluginDescriptor; +import org.pf4j.PluginManager; +import org.pf4j.PluginWrapper; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Mono; @@ -17,6 +25,9 @@ import run.halo.aifoundation.decision.DecisionModel; import run.halo.aifoundation.decision.DecisionRequest; import run.halo.aifoundation.decision.DecisionResponse; +import run.halo.aifoundation.service.audit.CallerPluginInfo; +import run.halo.aifoundation.service.audit.StackWalkingCallerPluginResolver; +import run.halo.app.extension.ReactiveExtensionClient; class DecisionConsoleEndpointTest { private final AiModelService service = mock(AiModelService.class); @@ -45,4 +56,47 @@ void resolvesResourceNameAndSuppliesLocalAuditMetadata() { assertThat(request.getValue().getState()).isEqualTo(Map.of("comment", "hello")); assertThat(request.getValue().getMetadata()).containsEntry("source", "console-test"); } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void detectsFoundationCallerWhenInvokingDecisionModel(boolean asyncResolution) { + var resolver = foundationCallerResolver(); + var caller = new AtomicReference(); + var model = mock(DecisionModel.class); + var resolution = Mono.just(model); + when(service.decisionModel("configured-model")).thenReturn(asyncResolution + ? resolution.publishOn(reactor.core.scheduler.Schedulers.boundedElastic()) : resolution); + when(model.decide(any())).thenAnswer(invocation -> { + caller.set(resolver.resolveCurrentCallerSnapshot()); + return Mono.just(new DecisionResponse("jev-version", Map.of(), null)); + }); + + client.post().uri("/models/configured-model/test-decision") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(Map.of("state", "Hello", + "questions", Map.of("q", Map.of("type", "YES_NO", "instructions", "Spam?")))) + .exchange().expectStatus().isOk(); + + assertThat(caller.get()).isNotNull(); + assertThat(caller.get().isDetected()).isTrue(); + assertThat(caller.get().getPluginName()).isEqualTo("ai-foundation"); + assertThat(caller.get().getVersion()).isEqualTo("1.0.0"); + assertThat(caller.get().getDetectionSource()).isEqualTo("stack-classloader"); + } + + @SuppressWarnings("unchecked") + private StackWalkingCallerPluginResolver foundationCallerResolver() { + var self = mock(PluginWrapper.class); + var manager = mock(PluginManager.class); + var descriptor = mock(PluginDescriptor.class); + var provider = mock(ObjectProvider.class); + when(provider.getIfAvailable()).thenReturn(self); + when(self.getPluginId()).thenReturn("ai-foundation"); + when(self.getPluginManager()).thenReturn(manager); + when(self.getPluginClassLoader()).thenReturn(DecisionConsoleEndpoint.class.getClassLoader()); + when(self.getDescriptor()).thenReturn(descriptor); + when(descriptor.getVersion()).thenReturn("1.0.0"); + when(manager.getStartedPlugins()).thenReturn(List.of(self)); + return new StackWalkingCallerPluginResolver(mock(ReactiveExtensionClient.class), provider); + } } From 55fdfe871a98d47fa668eec34e1bbf0295cb15b9 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 28 Oct 2026 17:07:38 +0800 Subject: [PATCH 06/10] Fix decision usage accounting, cancellation, and discovery URLs --- .../provider/typesafe/TypeSafeProvider.java | 4 +- .../service/decision/DecisionModelImpl.java | 8 ++- .../DecisionResponseValidationException.java | 25 +++++++ .../service/observation/NormalizedUsage.java | 4 ++ .../service/observation/UsageCallSession.java | 6 +- .../observation/UsageExecutionObserver.java | 13 +++- .../typesafe/TypeSafeDecisionClientTest.java | 30 ++++++-- .../audit/AuditedDecisionModelTest.java | 69 +++++++++++++++++++ dev/en/sdk-core/decision-models.md | 3 +- dev/zh-CN/sdk-core/decision-models.md | 2 +- ui/packages/sdk/src/decision-fetch.test.ts | 49 +++++++++++++ ui/packages/sdk/src/decision.test.ts | 25 ++++++- ui/packages/sdk/src/decision.ts | 3 +- 13 files changed, 226 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/run/halo/aifoundation/service/decision/DecisionResponseValidationException.java create mode 100644 ui/packages/sdk/src/decision-fetch.test.ts diff --git a/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java index 23052073..dc0a4fd3 100644 --- a/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java +++ b/app/src/main/java/run/halo/aifoundation/provider/typesafe/TypeSafeProvider.java @@ -18,6 +18,7 @@ import run.halo.aifoundation.provider.support.ModelType; import run.halo.aifoundation.provider.support.ProviderDecisionClient; import run.halo.aifoundation.provider.support.ProviderModelRef; +import run.halo.aifoundation.provider.support.ProviderUris; import run.halo.aifoundation.provider.transport.ProviderDiagnostics; import run.halo.aifoundation.provider.transport.ProviderHttpResponseSupport; @@ -112,7 +113,8 @@ public void validateNativeModelOptions(ProviderModelRef model) { @Override public Mono> discoverModels(AiProvider provider, String apiKey) { - var endpoint = resolveBaseUrl(provider).replaceAll("/+$", "") + "/v1/models"; + var endpoint = ProviderUris.withoutTrailingPath(resolveBaseUrl(provider), "/v1") + + "/v1/models"; var diagnostics = ProviderDiagnostics.create(getProviderType(), "models"); return discoveryWebClientBuilder(provider).build().get().uri(endpoint) .headers(headers -> headers.setBearerAuth(apiKey)) diff --git a/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java index fefb0a24..5f2a3f90 100644 --- a/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java +++ b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionModelImpl.java @@ -50,7 +50,13 @@ public Mono decide(DecisionRequest request) { var call = observer.observe(UsageUnitKind.DECISION, 0, () -> client.decide(request) .switchIfEmpty(Mono.error(new IllegalStateException("Empty decision response"))) - .doOnNext(response -> DecisionValidation.response(request, response)), + .doOnNext(response -> { + try { + DecisionValidation.response(request, response); + } catch (IllegalStateException error) { + throw new DecisionResponseValidationException(error, response); + } + }), response -> NormalizedUsage.from(response.getUsage()), DecisionResponse::getModel) .doOnNext(response -> checkCancellation(request)); var timeout = request.getTimeouts() == null ? null diff --git a/app/src/main/java/run/halo/aifoundation/service/decision/DecisionResponseValidationException.java b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionResponseValidationException.java new file mode 100644 index 00000000..3aa35adb --- /dev/null +++ b/app/src/main/java/run/halo/aifoundation/service/decision/DecisionResponseValidationException.java @@ -0,0 +1,25 @@ +package run.halo.aifoundation.service.decision; + +import run.halo.aifoundation.decision.DecisionResponse; +import run.halo.aifoundation.decision.DecisionUsage; + +/** Keeps reported usage when a decoded decision response fails semantic validation. */ +public final class DecisionResponseValidationException extends IllegalStateException { + private final String responseModel; + private final DecisionUsage usage; + + public DecisionResponseValidationException(IllegalStateException cause, + DecisionResponse response) { + super(cause.getMessage(), cause); + this.responseModel = response.getModel(); + this.usage = response.getUsage(); + } + + public String getResponseModel() { + return responseModel; + } + + public DecisionUsage getUsage() { + return usage; + } +} diff --git a/app/src/main/java/run/halo/aifoundation/service/observation/NormalizedUsage.java b/app/src/main/java/run/halo/aifoundation/service/observation/NormalizedUsage.java index eda795e0..623df39b 100644 --- a/app/src/main/java/run/halo/aifoundation/service/observation/NormalizedUsage.java +++ b/app/src/main/java/run/halo/aifoundation/service/observation/NormalizedUsage.java @@ -9,6 +9,7 @@ import run.halo.aifoundation.image.ImageUsage; import run.halo.aifoundation.provider.usage.ProviderUsage; import run.halo.aifoundation.rerank.RerankUsage; +import run.halo.aifoundation.service.decision.DecisionResponseValidationException; public record NormalizedUsage( Long inputTokens, @@ -114,6 +115,9 @@ public static NormalizedUsage fromFailure(Throwable error) { if (error == null) { break; } + if (error instanceof DecisionResponseValidationException e) { + return from(e.getUsage()); + } if (error instanceof run.halo.aifoundation.exception.StructuredOutputValidationException e) { return from(e.getUsage()); } diff --git a/app/src/main/java/run/halo/aifoundation/service/observation/UsageCallSession.java b/app/src/main/java/run/halo/aifoundation/service/observation/UsageCallSession.java index 8e694fdf..f23ad6fe 100644 --- a/app/src/main/java/run/halo/aifoundation/service/observation/UsageCallSession.java +++ b/app/src/main/java/run/halo/aifoundation/service/observation/UsageCallSession.java @@ -136,10 +136,10 @@ private synchronized void finish(UsageStatus status, UsageError error, var resolvedUsage = executionUsage == null ? NormalizedUsage.missing() : executionUsage; var resolvedResponseModelId = responseModelId; var resolvedStepCount = stepCount; + if (!StringUtils.hasText(resolvedResponseModelId)) { + resolvedResponseModelId = latestResponseModelId.get(); + } if (start.streaming()) { - if (!StringUtils.hasText(resolvedResponseModelId)) { - resolvedResponseModelId = latestResponseModelId.get(); - } resolvedStepCount = Math.max(resolvedStepCount, observedGenerationSteps.get()); } pendingTerminal = new UsageCallTerminal(start, completedAt, status, error, diff --git a/app/src/main/java/run/halo/aifoundation/service/observation/UsageExecutionObserver.java b/app/src/main/java/run/halo/aifoundation/service/observation/UsageExecutionObserver.java index 341e6e94..b7933e1a 100644 --- a/app/src/main/java/run/halo/aifoundation/service/observation/UsageExecutionObserver.java +++ b/app/src/main/java/run/halo/aifoundation/service/observation/UsageExecutionObserver.java @@ -7,6 +7,7 @@ import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import run.halo.aifoundation.service.decision.DecisionResponseValidationException; @Component public class UsageExecutionObserver { @@ -32,12 +33,22 @@ public Mono observe(UsageUnitKind kind, int unitIndex, Supplier> UsageTelemetry.safely(() -> responseModel.apply(value), null)); })) .doOnError(error -> UsageTelemetry.safely( - () -> scope.fail(error, NormalizedUsage.fromFailure(error), null))) + () -> scope.fail(error, NormalizedUsage.fromFailure(error), + responseModelFromFailure(error)))) .doOnCancel(() -> UsageTelemetry.safely( () -> scope.cancel(NormalizedUsage.missing(), null))); }); } + private static String responseModelFromFailure(Throwable error) { + for (int depth = 0; depth < 8 && error != null; depth++, error = error.getCause()) { + if (error instanceof DecisionResponseValidationException e) { + return e.getResponseModel(); + } + } + return null; + } + public Flux observeFlux(UsageUnitKind kind, int unitIndex, Supplier> invocation, Function usage, Function responseModel) { diff --git a/app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java b/app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java index 564feb59..a22a4aff 100644 --- a/app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java +++ b/app/src/test/java/run/halo/aifoundation/provider/typesafe/TypeSafeDecisionClientTest.java @@ -12,12 +12,16 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.web.reactive.function.client.WebClient; import reactor.test.StepVerifier; import run.halo.aifoundation.decision.DecisionQuestion; import run.halo.aifoundation.decision.DecisionRequest; import run.halo.aifoundation.extension.AiProvider; +import run.halo.aifoundation.provider.support.AdapterType; import run.halo.aifoundation.provider.support.ModelType; +import run.halo.aifoundation.provider.support.ProviderModelRef; import run.halo.aifoundation.provider.transport.ProviderHttpException; import run.halo.aifoundation.service.decision.DecisionModelImpl; import run.halo.aifoundation.service.observation.UsageExecutionObserver; @@ -93,21 +97,37 @@ void evaluatesMixedQuestionsAndPreservesProbabilitiesAndFractionalScore() { assertThat(requestBody.get()).doesNotContain("local-only", "metadata", "messages"); } - @Test - void discoversModelsUsingTypeSafeCatalogShape() { + @ParameterizedTest + @ValueSource(strings = {"", "/", "///", "/v1", "/v1/", "/v1///", "/proxy", "/proxy/v1/"}) + void normalizesDiscoveryAndInferenceBaseUrlsConsistently(String suffix) { response.set(""" {"models":[{"name":"jev-latest","description":"Decision model","release_date":"2026-09-15"}]} """); var provider = new AiProvider(); provider.setSpec(new AiProvider.AiProviderSpec()); - provider.getSpec().setBaseUrl(baseUrl + "/"); - StepVerifier.create(new TypeSafeProvider().discoverModels(provider, "test-key")) + provider.getSpec().setBaseUrl(baseUrl + suffix); + var type = new TypeSafeProvider(); + var prefix = suffix.startsWith("/proxy") ? "/proxy" : ""; + StepVerifier.create(type.discoverModels(provider, "test-key")) .assertNext(models -> { assertThat(models).hasSize(1); assertThat(models.getFirst().modelId()).isEqualTo("jev-latest"); assertThat(models.getFirst().modelType()).isEqualTo(ModelType.DECISION); }).verifyComplete(); - assertThat(path.get()).isEqualTo("/v1/models"); + assertThat(path.get()).isEqualTo(prefix + "/v1/models"); + assertThat(authorization.get()).isEqualTo("Bearer test-key"); + + response.set(""" + {"model":"jev-version","answers":{"spam":{"type":"noul","noul":0.2}}} + """); + var client = type.buildDecisionClient(provider, "test-key", + new ProviderModelRef("jev-latest", ModelType.DECISION, AdapterType.TYPESAFE_SYSTEM_ONE)); + StepVerifier.create(client.decide(DecisionRequest.builder().state("test") + .questions(Map.of("spam", DecisionQuestion.yesNo("Spam?"))).build())) + .assertNext(result -> assertThat(result.getAnswers().get("spam").getProbability()) + .isEqualTo(0.2)) + .verifyComplete(); + assertThat(path.get()).isEqualTo(prefix + "/v1/systemone"); assertThat(authorization.get()).isEqualTo("Bearer test-key"); } diff --git a/app/src/test/java/run/halo/aifoundation/service/audit/AuditedDecisionModelTest.java b/app/src/test/java/run/halo/aifoundation/service/audit/AuditedDecisionModelTest.java index 5429de2c..b808f643 100644 --- a/app/src/test/java/run/halo/aifoundation/service/audit/AuditedDecisionModelTest.java +++ b/app/src/test/java/run/halo/aifoundation/service/audit/AuditedDecisionModelTest.java @@ -10,10 +10,14 @@ import java.time.Clock; import java.time.Instant; import java.util.Map; +import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import run.halo.aifoundation.decision.DecisionAnswer; import run.halo.aifoundation.decision.DecisionQuestion; import run.halo.aifoundation.decision.DecisionQuestionType; @@ -26,6 +30,7 @@ import run.halo.aifoundation.service.observation.UsageCallDescriptor; import run.halo.aifoundation.service.observation.UsageCallSession; import run.halo.aifoundation.service.observation.UsageCallStart; +import run.halo.aifoundation.service.observation.UsageCallTerminal; import run.halo.aifoundation.service.observation.UsageExecutionObserver; import run.halo.aifoundation.service.observation.UsageExecutionRecord; import run.halo.aifoundation.service.observation.UsageQuality; @@ -34,6 +39,70 @@ import run.halo.aifoundation.service.usage.UsageStatisticsService; class AuditedDecisionModelTest { + @ParameterizedTest + @MethodSource("invalidResponseUsage") + void recordsFailedValidationWithReportedUsageAndActualModel(DecisionUsage usage, + Long input, Long output, Long total, UsageQuality quality) { + var statistics = mock(UsageStatisticsService.class); + var context = new ModelCallContext(ModelType.DECISION, + "decision-resource", "provider", "typesafe", "jev-latest"); + var descriptor = mock(UsageCallDescriptor.class); + var metadata = Map.of("feature", "moderation"); + var start = new UsageCallStart("decision-call", 1, Instant.now(), "plugin", "1", + "stack", null, "decision.decide", "decision", "decision-resource", + "provider", "typesafe", "jev-latest", false); + var session = new UsageCallSession(statistics, start, Clock.systemUTC()); + when(statistics.describeCall(context, "decision.decide", false, metadata)) + .thenReturn(descriptor); + when(statistics.beginCall(descriptor)).thenReturn(session); + var response = new DecisionResponse("jev-actual", Map.of("wrong-id", + DecisionAnswer.builder().type(DecisionQuestionType.YES_NO).probability(0.2).build()), + usage); + var runtime = new DecisionModelImpl(ignored -> Mono.just(response), + new UsageExecutionObserver(), new TypeSafeProvider().decisionCapabilities()); + var model = new AuditedDecisionModel(runtime, context, + mock(CallerPluginAuditRecorder.class), statistics); + + StepVerifier.create(model.decide(DecisionRequest.builder().state("comment") + .questions(Map.of("spam", DecisionQuestion.yesNo("Is it spam?"))) + .metadata(metadata).build())) + .expectErrorSatisfies(error -> assertThat(error) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Decision response question IDs do not match the request")) + .verify(); + + var execution = ArgumentCaptor.forClass(UsageExecutionRecord.class); + verify(statistics).recordExecution(eq(session), execution.capture()); + var record = execution.getValue(); + assertThat(record.status()).isEqualTo(UsageStatus.FAILED); + assertThat(record.requestModelId()).isEqualTo("jev-latest"); + assertThat(record.responseModelId()).isEqualTo("jev-actual"); + assertThat(record.usage().inputTokens()).isEqualTo(input); + assertThat(record.usage().outputTokens()).isEqualTo(output); + assertThat(record.usage().accountedTotalTokens()).isEqualTo(total); + assertThat(record.usage().quality()).isEqualTo(quality); + + var terminal = ArgumentCaptor.forClass(UsageCallTerminal.class); + verify(statistics).finishCall(eq(session), terminal.capture()); + assertThat(terminal.getValue().status()).isEqualTo(UsageStatus.FAILED); + assertThat(terminal.getValue().responseModelId()).isEqualTo("jev-actual"); + assertThat(terminal.getValue().attemptCount()).isEqualTo(1); + assertThat(terminal.getValue().missingExecutionCount()) + .isEqualTo(quality == UsageQuality.MISSING ? 1 : 0); + assertThat(terminal.getValue().usage()).isEqualTo(record.usage()); + } + + static Stream invalidResponseUsage() { + return Stream.of( + Arguments.of(new DecisionUsage(123, 22), 123L, 22L, 145L, + UsageQuality.REPORTED_COMPONENTS), + Arguments.of(new DecisionUsage(123, null), 123L, null, null, UsageQuality.PARTIAL), + Arguments.of(new DecisionUsage(null, 22), null, 22L, null, UsageQuality.PARTIAL), + Arguments.of(new DecisionUsage(0, 0), 0L, 0L, 0L, UsageQuality.REPORTED_COMPONENTS), + Arguments.of(new DecisionUsage(null, null), null, null, null, UsageQuality.MISSING), + Arguments.of(null, null, null, null, UsageQuality.MISSING)); + } + @ParameterizedTest @ValueSource(booleans = {true, false}) void recordsDecisionExecutionWithoutInventingMissingUsage(boolean hasUsage) { diff --git a/dev/en/sdk-core/decision-models.md b/dev/en/sdk-core/decision-models.md index 867741db..3ce1c55e 100644 --- a/dev/en/sdk-core/decision-models.md +++ b/dev/en/sdk-core/decision-models.md @@ -11,7 +11,8 @@ Java/npm SDK versions; the published `1.0.0` API does not include them. ## Configure TypeSafe / Jev Create a **TypeSafe (Jev)** provider and select a Halo Secret for its API key. The default service root -is `https://api.typesafe.ai`; the adapter appends `/v1/systemone` and `/v1/models`. Import a discovered +is `https://api.typesafe.ai`; URLs ending in `/v1` also work. Discovery and inference preserve proxy +path prefixes and append `/v1/models` and `/v1/systemone` without duplicating `/v1`. Import a discovered model or manually enter an accepted provider model ID with model type `decision` and adapter `typesafe-system-one`. Model discovery uses the account's catalog, not a hardcoded model list. Optionally configure the default decision model, then use the Console's Decision workbench. diff --git a/dev/zh-CN/sdk-core/decision-models.md b/dev/zh-CN/sdk-core/decision-models.md index cb1a80ed..0fa68bbd 100644 --- a/dev/zh-CN/sdk-core/decision-models.md +++ b/dev/zh-CN/sdk-core/decision-models.md @@ -11,7 +11,7 @@ ## 配置 TypeSafe / Jev 1. 在提供商管理中选择 **TypeSafe (Jev)**,通过 Halo Secret 配置 API Key。 -2. 默认 Base URL 为 `https://api.typesafe.ai`(服务根地址)。可配置代理;路径由适配器追加。 +2. 默认 Base URL 为 `https://api.typesafe.ai`(服务根地址),也支持以 `/v1` 结尾的地址。可配置代理;模型发现与决策调用都会保留路径前缀,且不会重复追加 `/v1`。 3. 从提供商发现并导入模型,或手动填写供应方允许的模型 ID。模型类型选择 `decision`,适配器为 `typesafe-system-one`。模型列表来自 `GET /v1/models`,没有硬编码版本列表。 4. 可在默认模型页设置默认决策模型,在工作台的“决策”页输入文本或 JSON,并添加多个问题。 diff --git a/ui/packages/sdk/src/decision-fetch.test.ts b/ui/packages/sdk/src/decision-fetch.test.ts new file mode 100644 index 00000000..08d7521f --- /dev/null +++ b/ui/packages/sdk/src/decision-fetch.test.ts @@ -0,0 +1,49 @@ +// @vitest-environment node +import { once } from 'node:events' +import { createServer } from 'node:http' +import { expect, it } from 'vitest' +import { createDecisionClient } from './decision' +import { AIUIProtocolError, isAbortError } from './errors' + +it('preserves cancellation while reading a real fetch response body', async () => { + const server = createServer((_request, response) => { + response.writeHead(200, { 'Content-Type': 'application/json' }) + response.flushHeaders() + response.write('{"model":"jev-1.14",') + }) + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const controller = new AbortController() + try { + const address = server.address() + if (!address || typeof address === 'string') throw new Error('Missing server address') + let headersReceived!: () => void + const received = new Promise((resolve) => { + headersReceived = resolve + }) + const client = createDecisionClient({ + api: `http://127.0.0.1:${address.port}/decision`, + fetch: async (input, init) => { + const response = await fetch(input, init) + headersReceived() + return response + }, + }) + const result = client + .decide( + { state: 'test', questions: { spam: { type: 'YES_NO', instructions: 'Spam?' } } }, + { signal: controller.signal }, + ) + .catch((error: unknown) => error) + await received + controller.abort() + const error = await result + expect(isAbortError(error)).toBe(true) + expect(error).toMatchObject({ name: 'AbortError' }) + expect(error).not.toBeInstanceOf(AIUIProtocolError) + } finally { + controller.abort() + server.closeAllConnections() + await new Promise((resolve) => server.close(() => resolve())) + } +}) diff --git a/ui/packages/sdk/src/decision.test.ts b/ui/packages/sdk/src/decision.test.ts index cd7ebb4c..9aec649c 100644 --- a/ui/packages/sdk/src/decision.test.ts +++ b/ui/packages/sdk/src/decision.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { createDecisionClient, type DecisionRequest } from './decision' -import { AIUIError, AIUIProtocolError } from './errors' +import { AIUIError, AIUIProtocolError, isAbortError } from './errors' const request: DecisionRequest = { state: { comment: 'hello' }, @@ -56,6 +56,29 @@ describe('decision client', () => { await expect(client.decide(request)).rejects.toMatchObject>({ status: 429 }) }) + it('reports malformed JSON as a protocol error', async () => { + const client = createDecisionClient({ + api: '/decision', + fetch: vi.fn().mockResolvedValue(new Response('{invalid json')), + }) + await expect(client.decide(request)).rejects.toMatchObject({ + name: 'AIUIProtocolError', + message: 'Decision endpoint returned invalid JSON', + cause: expect.any(SyntaxError), + }) + }) + + it('preserves cancellation before receiving response headers', async () => { + const error = new DOMException('Cancelled', 'AbortError') + const client = createDecisionClient({ + api: '/decision', + fetch: vi.fn().mockRejectedValue(error), + }) + const failure = await client.decide(request).catch((cause: unknown) => cause) + expect(failure).toBe(error) + expect(isAbortError(failure)).toBe(true) + }) + it('preserves choice distributions and fractional scores, rejecting incomplete distributions', async () => { const mixed: DecisionRequest = { state: ['comment'], diff --git a/ui/packages/sdk/src/decision.ts b/ui/packages/sdk/src/decision.ts index 18a105ed..8c528e3d 100644 --- a/ui/packages/sdk/src/decision.ts +++ b/ui/packages/sdk/src/decision.ts @@ -1,4 +1,4 @@ -import { AIUIError, AIUIProtocolError } from './errors' +import { AIUIError, AIUIProtocolError, isAbortError } from './errors' import type { FetchFunction } from './types' export type DecisionJsonValue = @@ -76,6 +76,7 @@ export function createDecisionClient(options: DecisionClientOptions) { try { result = await response.json() } catch (cause) { + if (isAbortError(cause)) throw cause throw new AIUIProtocolError('Decision endpoint returned invalid JSON', { cause }) } validateDecisionResponse(result, request) From 6a20e3d7b24a3813a6befd85534c40d0489278fe Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 28 Oct 2026 17:23:22 +0800 Subject: [PATCH 07/10] Align decision workbench layout with other test panels --- .../workbench/DecisionQuestionEditor.vue | 25 ++- .../components/workbench/DecisionResults.vue | 105 +++++++----- .../workbench/DecisionTestPanel.vue | 153 +++++++++++++----- 3 files changed, 198 insertions(+), 85 deletions(-) diff --git a/ui/src/views/components/workbench/DecisionQuestionEditor.vue b/ui/src/views/components/workbench/DecisionQuestionEditor.vue index 5ca64640..c74434f2 100644 --- a/ui/src/views/components/workbench/DecisionQuestionEditor.vue +++ b/ui/src/views/components/workbench/DecisionQuestionEditor.vue @@ -2,8 +2,9 @@ import { DecisionQuestionTypeEnum } from '@/api/generated' import { DECISION_QUESTION_TYPES, type DecisionQuestionDraft } from '@/utils/decision-workbench' import { VButton } from '@halo-dev/components' +import RiDeleteBinLine from '~icons/ri/delete-bin-line' -const props = defineProps<{ question: DecisionQuestionDraft; disabled: boolean }>() +const props = defineProps<{ question: DecisionQuestionDraft; index: number; disabled: boolean }>() const emit = defineEmits<{ update: [question: DecisionQuestionDraft]; remove: [] }>() function update(key: K, value: DecisionQuestionDraft[K]) { @@ -21,11 +22,25 @@ function updateType(value: unknown) { diff --git a/ui/src/views/components/workbench/DecisionResults.vue b/ui/src/views/components/workbench/DecisionResults.vue index b4ef43d2..e2d3df7d 100644 --- a/ui/src/views/components/workbench/DecisionResults.vue +++ b/ui/src/views/components/workbench/DecisionResults.vue @@ -1,60 +1,79 @@ diff --git a/ui/src/views/components/workbench/DecisionTestPanel.vue b/ui/src/views/components/workbench/DecisionTestPanel.vue index da264410..4070400d 100644 --- a/ui/src/views/components/workbench/DecisionTestPanel.vue +++ b/ui/src/views/components/workbench/DecisionTestPanel.vue @@ -2,6 +2,10 @@ import type { DecisionResponse } from '@/api/generated' import type { DecisionQuestionDraft } from '@/utils/decision-workbench' import { VButton } from '@halo-dev/components' +import RiAddLine from '~icons/ri/add-line' +import RiLoader4Line from '~icons/ri/loader-4-line' +import RiSendPlaneLine from '~icons/ri/send-plane-line' +import RiStackLine from '~icons/ri/stack-line' import DecisionQuestionEditor from './DecisionQuestionEditor.vue' import DecisionResults from './DecisionResults.vue' @@ -26,54 +30,131 @@ defineEmits<{ + + From d34a0baefccdaf8293bac256ed51b60f0c88ab7c Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 28 Oct 2026 22:43:43 +0800 Subject: [PATCH 08/10] Pin Halo tooling to 2.26.0 and regenerate API client --- api-docs/openapi/v3_0/aiFoundationApis.json | 2 +- app/build.gradle | 2 +- ui/src/api/generated/api.ts | 2 +- ui/src/api/generated/api/ai-model-v1alpha1-api.ts | 2 +- ui/src/api/generated/api/ai-provider-v1alpha1-api.ts | 2 +- ...sole-api-aifoundation-halo-run-v1alpha1-caller-plugin-api.ts | 2 +- ...api-aifoundation-halo-run-v1alpha1-default-model-slot-api.ts | 2 +- .../api/console-api-aifoundation-halo-run-v1alpha1-model-api.ts | 2 +- ...nsole-api-aifoundation-halo-run-v1alpha1-model-option-api.ts | 2 +- .../console-api-aifoundation-halo-run-v1alpha1-provider-api.ts | 2 +- ...sole-api-aifoundation-halo-run-v1alpha1-provider-type-api.ts | 2 +- ...e-api-aifoundation-halo-run-v1alpha1-usage-statistics-api.ts | 2 +- ui/src/api/generated/base.ts | 2 +- ui/src/api/generated/common.ts | 2 +- ui/src/api/generated/configuration.ts | 2 +- ui/src/api/generated/index.ts | 2 +- ui/src/api/generated/models/adapter-type-info.ts | 2 +- ui/src/api/generated/models/add-operation.ts | 2 +- ui/src/api/generated/models/ai-model-list.ts | 2 +- ui/src/api/generated/models/ai-model-spec.ts | 2 +- ui/src/api/generated/models/ai-model.ts | 2 +- ui/src/api/generated/models/ai-provider-list.ts | 2 +- ui/src/api/generated/models/ai-provider-spec.ts | 2 +- ui/src/api/generated/models/ai-provider-status.ts | 2 +- ui/src/api/generated/models/ai-provider.ts | 2 +- ui/src/api/generated/models/caller-plugin-info.ts | 2 +- ui/src/api/generated/models/copy-operation.ts | 2 +- ui/src/api/generated/models/data-content.ts | 2 +- ui/src/api/generated/models/decision-answer.ts | 2 +- ui/src/api/generated/models/decision-capability.ts | 2 +- ui/src/api/generated/models/decision-question.ts | 2 +- ui/src/api/generated/models/decision-response.ts | 2 +- ui/src/api/generated/models/decision-usage.ts | 2 +- ui/src/api/generated/models/default-model-slots.ts | 2 +- ui/src/api/generated/models/default-parameter-mapping-info.ts | 2 +- ui/src/api/generated/models/discovered-model-item.ts | 2 +- ui/src/api/generated/models/embedding-mappings.ts | 2 +- ui/src/api/generated/models/embedding-preview.ts | 2 +- ui/src/api/generated/models/embedding-response-metadata.ts | 2 +- ui/src/api/generated/models/embedding-usage.ts | 2 +- ui/src/api/generated/models/embedding-warning.ts | 2 +- ui/src/api/generated/models/generation-response-metadata.ts | 2 +- ui/src/api/generated/models/image-generation-capability.ts | 2 +- ui/src/api/generated/models/image-generation-mappings.ts | 2 +- ui/src/api/generated/models/image-generation-warning.ts | 2 +- ui/src/api/generated/models/image-usage.ts | 2 +- ui/src/api/generated/models/json-patch-inner.ts | 2 +- ui/src/api/generated/models/language-capability.ts | 2 +- ui/src/api/generated/models/language-mappings.ts | 2 +- ui/src/api/generated/models/metadata.ts | 2 +- ui/src/api/generated/models/model-capabilities.ts | 2 +- ui/src/api/generated/models/model-capability-sources.ts | 2 +- ui/src/api/generated/models/model-message-part.ts | 2 +- ui/src/api/generated/models/model-message.ts | 2 +- ui/src/api/generated/models/model-option-provider.ts | 2 +- ui/src/api/generated/models/model-option-unavailable-detail.ts | 2 +- ui/src/api/generated/models/model-option.ts | 2 +- ui/src/api/generated/models/model-parameter-definition-info.ts | 2 +- ui/src/api/generated/models/model-parameter-mappings.ts | 2 +- ui/src/api/generated/models/move-operation.ts | 2 +- ui/src/api/generated/models/normalized-usage.ts | 2 +- ui/src/api/generated/models/output-spec.ts | 2 +- ui/src/api/generated/models/parameter-mapping-template-info.ts | 2 +- .../api/generated/models/provider-model-discovery-response.ts | 2 +- ui/src/api/generated/models/provider-type-info.ts | 2 +- ui/src/api/generated/models/reasoning-mapping.ts | 2 +- ui/src/api/generated/models/reasoning-options.ts | 2 +- ui/src/api/generated/models/reasoning-value-mapping.ts | 2 +- ui/src/api/generated/models/remove-operation.ts | 2 +- ui/src/api/generated/models/replace-operation.ts | 2 +- ui/src/api/generated/models/rerank-mappings.ts | 2 +- ui/src/api/generated/models/rerank-preview.ts | 2 +- ui/src/api/generated/models/rerank-response-metadata.ts | 2 +- ui/src/api/generated/models/rerank-usage.ts | 2 +- ui/src/api/generated/models/rerank-warning.ts | 2 +- ui/src/api/generated/models/reset-request.ts | 2 +- ui/src/api/generated/models/selection.ts | 2 +- ui/src/api/generated/models/test-completion-stream-request.ts | 2 +- ui/src/api/generated/models/test-decision-request.ts | 2 +- ui/src/api/generated/models/test-embedding-request.ts | 2 +- ui/src/api/generated/models/test-embedding-response.ts | 2 +- ui/src/api/generated/models/test-generated-file.ts | 2 +- ui/src/api/generated/models/test-image-generation-request.ts | 2 +- ui/src/api/generated/models/test-image-generation-response.ts | 2 +- ui/src/api/generated/models/test-media-content.ts | 2 +- ui/src/api/generated/models/test-object-stream-request.ts | 2 +- ui/src/api/generated/models/test-operation.ts | 2 +- ui/src/api/generated/models/test-rag-options.ts | 2 +- ui/src/api/generated/models/test-rag-request.ts | 2 +- ui/src/api/generated/models/test-rag-source.ts | 2 +- ui/src/api/generated/models/test-rerank-request.ts | 2 +- ui/src/api/generated/models/test-rerank-response.ts | 2 +- ui/src/api/generated/models/test-ui-message-chat-request.ts | 2 +- ui/src/api/generated/models/test-ui-message.ts | 2 +- ui/src/api/generated/models/tool-choice.ts | 2 +- ui/src/api/generated/models/uimessage-chunk.ts | 2 +- ui/src/api/generated/models/usage-breakdown-item.ts | 2 +- ui/src/api/generated/models/usage-breakdown.ts | 2 +- ui/src/api/generated/models/usage-call-detail.ts | 2 +- ui/src/api/generated/models/usage-call-item.ts | 2 +- ui/src/api/generated/models/usage-call-page.ts | 2 +- ui/src/api/generated/models/usage-error.ts | 2 +- ui/src/api/generated/models/usage-execution-record.ts | 2 +- ui/src/api/generated/models/usage-health.ts | 2 +- ui/src/api/generated/models/usage-summary.ts | 2 +- ui/src/api/generated/models/usage-trend-dimension-value.ts | 2 +- ui/src/api/generated/models/usage-trend-point.ts | 2 +- 107 files changed, 107 insertions(+), 107 deletions(-) diff --git a/api-docs/openapi/v3_0/aiFoundationApis.json b/api-docs/openapi/v3_0/aiFoundationApis.json index 3f93bd6c..86123e99 100644 --- a/api-docs/openapi/v3_0/aiFoundationApis.json +++ b/api-docs/openapi/v3_0/aiFoundationApis.json @@ -2,7 +2,7 @@ "openapi" : "3.0.1", "info" : { "title" : "Halo", - "version" : "2.26.1" + "version" : "2.26.0" }, "servers" : [ { "url" : "/", diff --git a/app/build.gradle b/app/build.gradle index 03cc7990..c054883a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -113,7 +113,7 @@ haloPlugin { } halo { - version = '2.26' + version = '2.26.0' } def generatedOpenApi = layout.projectDirectory.file( diff --git a/ui/src/api/generated/api.ts b/ui/src/api/generated/api.ts index ee18b40d..3654e1fa 100644 --- a/ui/src/api/generated/api.ts +++ b/ui/src/api/generated/api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/ai-model-v1alpha1-api.ts b/ui/src/api/generated/api/ai-model-v1alpha1-api.ts index e86d4011..b5989fee 100644 --- a/ui/src/api/generated/api/ai-model-v1alpha1-api.ts +++ b/ui/src/api/generated/api/ai-model-v1alpha1-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/ai-provider-v1alpha1-api.ts b/ui/src/api/generated/api/ai-provider-v1alpha1-api.ts index 3a99af89..e92a112b 100644 --- a/ui/src/api/generated/api/ai-provider-v1alpha1-api.ts +++ b/ui/src/api/generated/api/ai-provider-v1alpha1-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-caller-plugin-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-caller-plugin-api.ts index 189626fd..572931e5 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-caller-plugin-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-caller-plugin-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-default-model-slot-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-default-model-slot-api.ts index c159ecee..34ea5e58 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-default-model-slot-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-default-model-slot-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-api.ts index 9b65d9e5..5a43d94e 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-option-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-option-api.ts index 357616ac..88f6425c 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-option-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-model-option-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-api.ts index 958dbc45..1c8c45d2 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-type-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-type-api.ts index eefc9fdd..83138ccb 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-type-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-provider-type-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-usage-statistics-api.ts b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-usage-statistics-api.ts index e44dd97d..83bea5b9 100644 --- a/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-usage-statistics-api.ts +++ b/ui/src/api/generated/api/console-api-aifoundation-halo-run-v1alpha1-usage-statistics-api.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/base.ts b/ui/src/api/generated/base.ts index abe8c5ff..8dc54c08 100644 --- a/ui/src/api/generated/base.ts +++ b/ui/src/api/generated/base.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/common.ts b/ui/src/api/generated/common.ts index c5d206d6..d58f5967 100644 --- a/ui/src/api/generated/common.ts +++ b/ui/src/api/generated/common.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/configuration.ts b/ui/src/api/generated/configuration.ts index 56b807da..539aa90d 100644 --- a/ui/src/api/generated/configuration.ts +++ b/ui/src/api/generated/configuration.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/index.ts b/ui/src/api/generated/index.ts index 05d0cb9a..8525548c 100644 --- a/ui/src/api/generated/index.ts +++ b/ui/src/api/generated/index.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/adapter-type-info.ts b/ui/src/api/generated/models/adapter-type-info.ts index 4404576d..61676e92 100644 --- a/ui/src/api/generated/models/adapter-type-info.ts +++ b/ui/src/api/generated/models/adapter-type-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/add-operation.ts b/ui/src/api/generated/models/add-operation.ts index 54382db1..124f7bca 100644 --- a/ui/src/api/generated/models/add-operation.ts +++ b/ui/src/api/generated/models/add-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-model-list.ts b/ui/src/api/generated/models/ai-model-list.ts index c839c4b9..d2d0018c 100644 --- a/ui/src/api/generated/models/ai-model-list.ts +++ b/ui/src/api/generated/models/ai-model-list.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-model-spec.ts b/ui/src/api/generated/models/ai-model-spec.ts index e70d5370..eb6f276e 100644 --- a/ui/src/api/generated/models/ai-model-spec.ts +++ b/ui/src/api/generated/models/ai-model-spec.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-model.ts b/ui/src/api/generated/models/ai-model.ts index 98499e5a..6881905e 100644 --- a/ui/src/api/generated/models/ai-model.ts +++ b/ui/src/api/generated/models/ai-model.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-provider-list.ts b/ui/src/api/generated/models/ai-provider-list.ts index bb4e6839..1fc03045 100644 --- a/ui/src/api/generated/models/ai-provider-list.ts +++ b/ui/src/api/generated/models/ai-provider-list.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-provider-spec.ts b/ui/src/api/generated/models/ai-provider-spec.ts index 0cfaae30..65998812 100644 --- a/ui/src/api/generated/models/ai-provider-spec.ts +++ b/ui/src/api/generated/models/ai-provider-spec.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-provider-status.ts b/ui/src/api/generated/models/ai-provider-status.ts index 5dbfef31..5b5b4e8e 100644 --- a/ui/src/api/generated/models/ai-provider-status.ts +++ b/ui/src/api/generated/models/ai-provider-status.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/ai-provider.ts b/ui/src/api/generated/models/ai-provider.ts index 43ebbd86..233a300a 100644 --- a/ui/src/api/generated/models/ai-provider.ts +++ b/ui/src/api/generated/models/ai-provider.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/caller-plugin-info.ts b/ui/src/api/generated/models/caller-plugin-info.ts index 6b688623..e741f920 100644 --- a/ui/src/api/generated/models/caller-plugin-info.ts +++ b/ui/src/api/generated/models/caller-plugin-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/copy-operation.ts b/ui/src/api/generated/models/copy-operation.ts index 5749469d..ffb9d89d 100644 --- a/ui/src/api/generated/models/copy-operation.ts +++ b/ui/src/api/generated/models/copy-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/data-content.ts b/ui/src/api/generated/models/data-content.ts index 9e3470a8..3c8feaa5 100644 --- a/ui/src/api/generated/models/data-content.ts +++ b/ui/src/api/generated/models/data-content.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/decision-answer.ts b/ui/src/api/generated/models/decision-answer.ts index e80daab0..6fad7776 100644 --- a/ui/src/api/generated/models/decision-answer.ts +++ b/ui/src/api/generated/models/decision-answer.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/decision-capability.ts b/ui/src/api/generated/models/decision-capability.ts index 37ab275f..b4beeb57 100644 --- a/ui/src/api/generated/models/decision-capability.ts +++ b/ui/src/api/generated/models/decision-capability.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/decision-question.ts b/ui/src/api/generated/models/decision-question.ts index 82cbc02b..5c95dafd 100644 --- a/ui/src/api/generated/models/decision-question.ts +++ b/ui/src/api/generated/models/decision-question.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/decision-response.ts b/ui/src/api/generated/models/decision-response.ts index aadcee6c..ed4325e4 100644 --- a/ui/src/api/generated/models/decision-response.ts +++ b/ui/src/api/generated/models/decision-response.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/decision-usage.ts b/ui/src/api/generated/models/decision-usage.ts index cc7eaeaf..c28eee09 100644 --- a/ui/src/api/generated/models/decision-usage.ts +++ b/ui/src/api/generated/models/decision-usage.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/default-model-slots.ts b/ui/src/api/generated/models/default-model-slots.ts index 6de04509..3801a8e8 100644 --- a/ui/src/api/generated/models/default-model-slots.ts +++ b/ui/src/api/generated/models/default-model-slots.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/default-parameter-mapping-info.ts b/ui/src/api/generated/models/default-parameter-mapping-info.ts index 684a5366..66ab1fce 100644 --- a/ui/src/api/generated/models/default-parameter-mapping-info.ts +++ b/ui/src/api/generated/models/default-parameter-mapping-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/discovered-model-item.ts b/ui/src/api/generated/models/discovered-model-item.ts index 11748a4b..67d0694a 100644 --- a/ui/src/api/generated/models/discovered-model-item.ts +++ b/ui/src/api/generated/models/discovered-model-item.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/embedding-mappings.ts b/ui/src/api/generated/models/embedding-mappings.ts index 4f3d5f6d..a988128a 100644 --- a/ui/src/api/generated/models/embedding-mappings.ts +++ b/ui/src/api/generated/models/embedding-mappings.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/embedding-preview.ts b/ui/src/api/generated/models/embedding-preview.ts index f00d6680..b57bcc8c 100644 --- a/ui/src/api/generated/models/embedding-preview.ts +++ b/ui/src/api/generated/models/embedding-preview.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/embedding-response-metadata.ts b/ui/src/api/generated/models/embedding-response-metadata.ts index b424bb77..00bb74cc 100644 --- a/ui/src/api/generated/models/embedding-response-metadata.ts +++ b/ui/src/api/generated/models/embedding-response-metadata.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/embedding-usage.ts b/ui/src/api/generated/models/embedding-usage.ts index 67126a65..d242a940 100644 --- a/ui/src/api/generated/models/embedding-usage.ts +++ b/ui/src/api/generated/models/embedding-usage.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/embedding-warning.ts b/ui/src/api/generated/models/embedding-warning.ts index 063a16e5..d1fdf73e 100644 --- a/ui/src/api/generated/models/embedding-warning.ts +++ b/ui/src/api/generated/models/embedding-warning.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/generation-response-metadata.ts b/ui/src/api/generated/models/generation-response-metadata.ts index fe4c0652..029e555a 100644 --- a/ui/src/api/generated/models/generation-response-metadata.ts +++ b/ui/src/api/generated/models/generation-response-metadata.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/image-generation-capability.ts b/ui/src/api/generated/models/image-generation-capability.ts index c1aca969..1e66de41 100644 --- a/ui/src/api/generated/models/image-generation-capability.ts +++ b/ui/src/api/generated/models/image-generation-capability.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/image-generation-mappings.ts b/ui/src/api/generated/models/image-generation-mappings.ts index d6d2a0f1..f65fa219 100644 --- a/ui/src/api/generated/models/image-generation-mappings.ts +++ b/ui/src/api/generated/models/image-generation-mappings.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/image-generation-warning.ts b/ui/src/api/generated/models/image-generation-warning.ts index 2babde6b..d4a77ed7 100644 --- a/ui/src/api/generated/models/image-generation-warning.ts +++ b/ui/src/api/generated/models/image-generation-warning.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/image-usage.ts b/ui/src/api/generated/models/image-usage.ts index 74386c57..7cfd002a 100644 --- a/ui/src/api/generated/models/image-usage.ts +++ b/ui/src/api/generated/models/image-usage.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/json-patch-inner.ts b/ui/src/api/generated/models/json-patch-inner.ts index e1ea2224..2b1c9d4d 100644 --- a/ui/src/api/generated/models/json-patch-inner.ts +++ b/ui/src/api/generated/models/json-patch-inner.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/language-capability.ts b/ui/src/api/generated/models/language-capability.ts index d37b2e5f..c2fbec5b 100644 --- a/ui/src/api/generated/models/language-capability.ts +++ b/ui/src/api/generated/models/language-capability.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/language-mappings.ts b/ui/src/api/generated/models/language-mappings.ts index 91b6945e..c6ec1a04 100644 --- a/ui/src/api/generated/models/language-mappings.ts +++ b/ui/src/api/generated/models/language-mappings.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/metadata.ts b/ui/src/api/generated/models/metadata.ts index 7f326d7d..f7da9aeb 100644 --- a/ui/src/api/generated/models/metadata.ts +++ b/ui/src/api/generated/models/metadata.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-capabilities.ts b/ui/src/api/generated/models/model-capabilities.ts index a07c6ec8..ab28c21c 100644 --- a/ui/src/api/generated/models/model-capabilities.ts +++ b/ui/src/api/generated/models/model-capabilities.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-capability-sources.ts b/ui/src/api/generated/models/model-capability-sources.ts index 8e8b60c0..07eff0be 100644 --- a/ui/src/api/generated/models/model-capability-sources.ts +++ b/ui/src/api/generated/models/model-capability-sources.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-message-part.ts b/ui/src/api/generated/models/model-message-part.ts index 9b11ccd2..a4e662f4 100644 --- a/ui/src/api/generated/models/model-message-part.ts +++ b/ui/src/api/generated/models/model-message-part.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-message.ts b/ui/src/api/generated/models/model-message.ts index 6fd6ea8a..0e404902 100644 --- a/ui/src/api/generated/models/model-message.ts +++ b/ui/src/api/generated/models/model-message.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-option-provider.ts b/ui/src/api/generated/models/model-option-provider.ts index de99d78f..ffd5342e 100644 --- a/ui/src/api/generated/models/model-option-provider.ts +++ b/ui/src/api/generated/models/model-option-provider.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-option-unavailable-detail.ts b/ui/src/api/generated/models/model-option-unavailable-detail.ts index 35d67db6..3d763af2 100644 --- a/ui/src/api/generated/models/model-option-unavailable-detail.ts +++ b/ui/src/api/generated/models/model-option-unavailable-detail.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-option.ts b/ui/src/api/generated/models/model-option.ts index 4e5843f1..16f26afc 100644 --- a/ui/src/api/generated/models/model-option.ts +++ b/ui/src/api/generated/models/model-option.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-parameter-definition-info.ts b/ui/src/api/generated/models/model-parameter-definition-info.ts index a738ab5e..b9fa7c81 100644 --- a/ui/src/api/generated/models/model-parameter-definition-info.ts +++ b/ui/src/api/generated/models/model-parameter-definition-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/model-parameter-mappings.ts b/ui/src/api/generated/models/model-parameter-mappings.ts index dc6e5b8c..71810e55 100644 --- a/ui/src/api/generated/models/model-parameter-mappings.ts +++ b/ui/src/api/generated/models/model-parameter-mappings.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/move-operation.ts b/ui/src/api/generated/models/move-operation.ts index f458b49b..b2670fae 100644 --- a/ui/src/api/generated/models/move-operation.ts +++ b/ui/src/api/generated/models/move-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/normalized-usage.ts b/ui/src/api/generated/models/normalized-usage.ts index 195ccf0a..9be00d1a 100644 --- a/ui/src/api/generated/models/normalized-usage.ts +++ b/ui/src/api/generated/models/normalized-usage.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/output-spec.ts b/ui/src/api/generated/models/output-spec.ts index 59393c26..918f5df1 100644 --- a/ui/src/api/generated/models/output-spec.ts +++ b/ui/src/api/generated/models/output-spec.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/parameter-mapping-template-info.ts b/ui/src/api/generated/models/parameter-mapping-template-info.ts index 1e93ff24..f0d9c8e2 100644 --- a/ui/src/api/generated/models/parameter-mapping-template-info.ts +++ b/ui/src/api/generated/models/parameter-mapping-template-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/provider-model-discovery-response.ts b/ui/src/api/generated/models/provider-model-discovery-response.ts index 70fc6126..701940f9 100644 --- a/ui/src/api/generated/models/provider-model-discovery-response.ts +++ b/ui/src/api/generated/models/provider-model-discovery-response.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/provider-type-info.ts b/ui/src/api/generated/models/provider-type-info.ts index 28c48ec5..c2876616 100644 --- a/ui/src/api/generated/models/provider-type-info.ts +++ b/ui/src/api/generated/models/provider-type-info.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/reasoning-mapping.ts b/ui/src/api/generated/models/reasoning-mapping.ts index 609c8124..c1c26fd8 100644 --- a/ui/src/api/generated/models/reasoning-mapping.ts +++ b/ui/src/api/generated/models/reasoning-mapping.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/reasoning-options.ts b/ui/src/api/generated/models/reasoning-options.ts index ec347b1e..2ab3d4e4 100644 --- a/ui/src/api/generated/models/reasoning-options.ts +++ b/ui/src/api/generated/models/reasoning-options.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/reasoning-value-mapping.ts b/ui/src/api/generated/models/reasoning-value-mapping.ts index e3237658..6c4d4f3e 100644 --- a/ui/src/api/generated/models/reasoning-value-mapping.ts +++ b/ui/src/api/generated/models/reasoning-value-mapping.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/remove-operation.ts b/ui/src/api/generated/models/remove-operation.ts index 16b57b51..6c413d06 100644 --- a/ui/src/api/generated/models/remove-operation.ts +++ b/ui/src/api/generated/models/remove-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/replace-operation.ts b/ui/src/api/generated/models/replace-operation.ts index 99d981be..e8724043 100644 --- a/ui/src/api/generated/models/replace-operation.ts +++ b/ui/src/api/generated/models/replace-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/rerank-mappings.ts b/ui/src/api/generated/models/rerank-mappings.ts index 4c6afe57..a095f008 100644 --- a/ui/src/api/generated/models/rerank-mappings.ts +++ b/ui/src/api/generated/models/rerank-mappings.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/rerank-preview.ts b/ui/src/api/generated/models/rerank-preview.ts index 8df35037..c41d8a82 100644 --- a/ui/src/api/generated/models/rerank-preview.ts +++ b/ui/src/api/generated/models/rerank-preview.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/rerank-response-metadata.ts b/ui/src/api/generated/models/rerank-response-metadata.ts index a624cae6..05e0945f 100644 --- a/ui/src/api/generated/models/rerank-response-metadata.ts +++ b/ui/src/api/generated/models/rerank-response-metadata.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/rerank-usage.ts b/ui/src/api/generated/models/rerank-usage.ts index e1b8674e..bfc1c651 100644 --- a/ui/src/api/generated/models/rerank-usage.ts +++ b/ui/src/api/generated/models/rerank-usage.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/rerank-warning.ts b/ui/src/api/generated/models/rerank-warning.ts index f4e8ef8c..e4a052b2 100644 --- a/ui/src/api/generated/models/rerank-warning.ts +++ b/ui/src/api/generated/models/rerank-warning.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/reset-request.ts b/ui/src/api/generated/models/reset-request.ts index e913a6b5..541d21ad 100644 --- a/ui/src/api/generated/models/reset-request.ts +++ b/ui/src/api/generated/models/reset-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/selection.ts b/ui/src/api/generated/models/selection.ts index 9c38d35f..c5167bb0 100644 --- a/ui/src/api/generated/models/selection.ts +++ b/ui/src/api/generated/models/selection.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-completion-stream-request.ts b/ui/src/api/generated/models/test-completion-stream-request.ts index 016ba7c1..02714b4b 100644 --- a/ui/src/api/generated/models/test-completion-stream-request.ts +++ b/ui/src/api/generated/models/test-completion-stream-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-decision-request.ts b/ui/src/api/generated/models/test-decision-request.ts index 74350d69..9b2258c8 100644 --- a/ui/src/api/generated/models/test-decision-request.ts +++ b/ui/src/api/generated/models/test-decision-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-embedding-request.ts b/ui/src/api/generated/models/test-embedding-request.ts index f09cad95..9f6d4bf5 100644 --- a/ui/src/api/generated/models/test-embedding-request.ts +++ b/ui/src/api/generated/models/test-embedding-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-embedding-response.ts b/ui/src/api/generated/models/test-embedding-response.ts index de5d7e99..ed4baa14 100644 --- a/ui/src/api/generated/models/test-embedding-response.ts +++ b/ui/src/api/generated/models/test-embedding-response.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-generated-file.ts b/ui/src/api/generated/models/test-generated-file.ts index b6f71672..d6c3a386 100644 --- a/ui/src/api/generated/models/test-generated-file.ts +++ b/ui/src/api/generated/models/test-generated-file.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-image-generation-request.ts b/ui/src/api/generated/models/test-image-generation-request.ts index 8ceb80db..c3cbf07e 100644 --- a/ui/src/api/generated/models/test-image-generation-request.ts +++ b/ui/src/api/generated/models/test-image-generation-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-image-generation-response.ts b/ui/src/api/generated/models/test-image-generation-response.ts index 0fc9af75..2cc90eb4 100644 --- a/ui/src/api/generated/models/test-image-generation-response.ts +++ b/ui/src/api/generated/models/test-image-generation-response.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-media-content.ts b/ui/src/api/generated/models/test-media-content.ts index 473dd57c..1fd467a1 100644 --- a/ui/src/api/generated/models/test-media-content.ts +++ b/ui/src/api/generated/models/test-media-content.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-object-stream-request.ts b/ui/src/api/generated/models/test-object-stream-request.ts index 080716b9..b6f6f327 100644 --- a/ui/src/api/generated/models/test-object-stream-request.ts +++ b/ui/src/api/generated/models/test-object-stream-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-operation.ts b/ui/src/api/generated/models/test-operation.ts index 74efa916..73dedea5 100644 --- a/ui/src/api/generated/models/test-operation.ts +++ b/ui/src/api/generated/models/test-operation.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-rag-options.ts b/ui/src/api/generated/models/test-rag-options.ts index bcd75115..43d74a04 100644 --- a/ui/src/api/generated/models/test-rag-options.ts +++ b/ui/src/api/generated/models/test-rag-options.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-rag-request.ts b/ui/src/api/generated/models/test-rag-request.ts index 52179675..9640e2fd 100644 --- a/ui/src/api/generated/models/test-rag-request.ts +++ b/ui/src/api/generated/models/test-rag-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-rag-source.ts b/ui/src/api/generated/models/test-rag-source.ts index 790ddc2f..e0a7519a 100644 --- a/ui/src/api/generated/models/test-rag-source.ts +++ b/ui/src/api/generated/models/test-rag-source.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-rerank-request.ts b/ui/src/api/generated/models/test-rerank-request.ts index d5e1c699..2728ad7f 100644 --- a/ui/src/api/generated/models/test-rerank-request.ts +++ b/ui/src/api/generated/models/test-rerank-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-rerank-response.ts b/ui/src/api/generated/models/test-rerank-response.ts index 3fbfd2f6..67130829 100644 --- a/ui/src/api/generated/models/test-rerank-response.ts +++ b/ui/src/api/generated/models/test-rerank-response.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-ui-message-chat-request.ts b/ui/src/api/generated/models/test-ui-message-chat-request.ts index 42d6faa0..334dda2e 100644 --- a/ui/src/api/generated/models/test-ui-message-chat-request.ts +++ b/ui/src/api/generated/models/test-ui-message-chat-request.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/test-ui-message.ts b/ui/src/api/generated/models/test-ui-message.ts index 12d4b24c..849e5783 100644 --- a/ui/src/api/generated/models/test-ui-message.ts +++ b/ui/src/api/generated/models/test-ui-message.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/tool-choice.ts b/ui/src/api/generated/models/tool-choice.ts index 43f901cd..c6fec7f1 100644 --- a/ui/src/api/generated/models/tool-choice.ts +++ b/ui/src/api/generated/models/tool-choice.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/uimessage-chunk.ts b/ui/src/api/generated/models/uimessage-chunk.ts index d62119aa..82abd4bd 100644 --- a/ui/src/api/generated/models/uimessage-chunk.ts +++ b/ui/src/api/generated/models/uimessage-chunk.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-breakdown-item.ts b/ui/src/api/generated/models/usage-breakdown-item.ts index 85a6353a..9f9ed541 100644 --- a/ui/src/api/generated/models/usage-breakdown-item.ts +++ b/ui/src/api/generated/models/usage-breakdown-item.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-breakdown.ts b/ui/src/api/generated/models/usage-breakdown.ts index b82af1eb..82eca11c 100644 --- a/ui/src/api/generated/models/usage-breakdown.ts +++ b/ui/src/api/generated/models/usage-breakdown.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-call-detail.ts b/ui/src/api/generated/models/usage-call-detail.ts index e251fb18..b4b10a1a 100644 --- a/ui/src/api/generated/models/usage-call-detail.ts +++ b/ui/src/api/generated/models/usage-call-detail.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-call-item.ts b/ui/src/api/generated/models/usage-call-item.ts index 6550c229..05315d78 100644 --- a/ui/src/api/generated/models/usage-call-item.ts +++ b/ui/src/api/generated/models/usage-call-item.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-call-page.ts b/ui/src/api/generated/models/usage-call-page.ts index f16c9ef1..10bbbe1a 100644 --- a/ui/src/api/generated/models/usage-call-page.ts +++ b/ui/src/api/generated/models/usage-call-page.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-error.ts b/ui/src/api/generated/models/usage-error.ts index 703c6246..72d45158 100644 --- a/ui/src/api/generated/models/usage-error.ts +++ b/ui/src/api/generated/models/usage-error.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-execution-record.ts b/ui/src/api/generated/models/usage-execution-record.ts index b4f4b61e..bfa701c7 100644 --- a/ui/src/api/generated/models/usage-execution-record.ts +++ b/ui/src/api/generated/models/usage-execution-record.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-health.ts b/ui/src/api/generated/models/usage-health.ts index 68e01d83..604309b9 100644 --- a/ui/src/api/generated/models/usage-health.ts +++ b/ui/src/api/generated/models/usage-health.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-summary.ts b/ui/src/api/generated/models/usage-summary.ts index 05ca8e74..ef034196 100644 --- a/ui/src/api/generated/models/usage-summary.ts +++ b/ui/src/api/generated/models/usage-summary.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-trend-dimension-value.ts b/ui/src/api/generated/models/usage-trend-dimension-value.ts index 1fcd9feb..5e2171cc 100644 --- a/ui/src/api/generated/models/usage-trend-dimension-value.ts +++ b/ui/src/api/generated/models/usage-trend-dimension-value.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/ui/src/api/generated/models/usage-trend-point.ts b/ui/src/api/generated/models/usage-trend-point.ts index 4bdd5889..4a8a2a3b 100644 --- a/ui/src/api/generated/models/usage-trend-point.ts +++ b/ui/src/api/generated/models/usage-trend-point.ts @@ -4,7 +4,7 @@ * Halo * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * - * The version of the OpenAPI document: 2.26.1 + * The version of the OpenAPI document: 2.26.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). From 4457f5eaac3c7776baf81a68b65166768c6b388a Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Thu, 29 Oct 2026 15:34:11 +0800 Subject: [PATCH 09/10] Remove AIHubMix-specific Jev documentation section --- dev/en/sdk-core/decision-models.md | 12 ------------ dev/zh-CN/sdk-core/decision-models.md | 10 ---------- 2 files changed, 22 deletions(-) diff --git a/dev/en/sdk-core/decision-models.md b/dev/en/sdk-core/decision-models.md index 3ce1c55e..ef55e78c 100644 --- a/dev/en/sdk-core/decision-models.md +++ b/dev/en/sdk-core/decision-models.md @@ -22,18 +22,6 @@ The adapter supports text/JSON state and `YES_NO`, `CHOICE`, and `SCORE` questio and automatic business routing are outside this adapter's current scope. Unsupported native options are rejected explicitly. -## Use Jev through AIHubMix - -Create an **AIHubMix** provider with its API key and the default Base URL `https://aihubmix.com/v1`. -Refresh model discovery and import a Jev model returned by the catalog, such as `jev-latest`. -Catalog entries with type `decision` map to decision models with adapter `aihubmix-system-one`. -Manual entry of an available model ID is also supported; discovery does not hardcode Jev versions. - -Requests use AIHubMix's `POST /v1/systemone`, bearer authentication, and `APP-Code` header. -Both service-root and `/v1` Base URLs work without duplicating `/v1`. The supported question types -and limits match the System One adapter above. Imported models can run in the Decision workbench -without creating a separate TypeSafe provider. - ## Java ```java diff --git a/dev/zh-CN/sdk-core/decision-models.md b/dev/zh-CN/sdk-core/decision-models.md index 0fa68bbd..788de34e 100644 --- a/dev/zh-CN/sdk-core/decision-models.md +++ b/dev/zh-CN/sdk-core/decision-models.md @@ -20,16 +20,6 @@ 评分问题为 2–10 个有序等级。不提供图片/Box、文本生成、工具执行或自动业务路由。 原生参数不支持时会明确拒绝,不会静默忽略。 -## 通过 AIHubMix 使用 Jev - -在提供商管理中选择 **AIHubMix** 并配置它的 API Key,默认 Base URL 为 `https://aihubmix.com/v1`。 -打开模型发现并刷新列表,导入目录返回的 Jev 模型(如 `jev-latest`)。目录中的 `decision` 类型会映射为 -决策模型,适配器为 `aihubmix-system-one`;也支持手动填写目录中可用的模型 ID,不限定 Jev 版本。 - -调用使用 AIHubMix 的 `POST /v1/systemone`,带上 AIHubMix 的鉴权与 `APP-Code` 请求头。 -服务根地址和以 `/v1` 结尾的 Base URL 均可使用;适配器不会重复追加 `/v1`。支持的问题类型和限制与 -上述 System One 适配一致,导入后可直接在“决策”工作台测试,无需另建 TypeSafe 提供商。 - ## Java 调用 ```java From bf64a13df3c6d0c05e7567668bfe7698f6252458 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Thu, 29 Oct 2026 15:38:51 +0800 Subject: [PATCH 10/10] Keep decision model documentation provider-neutral --- README.md | 2 +- README_EN.md | 2 +- dev/en/sdk-core/decision-models.md | 34 +++++++++------------------ dev/zh-CN/sdk-core/decision-models.md | 30 +++++++---------------- 4 files changed, 22 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index fb2b3402..adff460e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Halo 官方 AI 能力平台,统一接入主流大模型,为插件生态提 ## 功能特性 - **多提供商支持**:内置 OpenAI、OpenRouter、DeepSeek、月之暗面 Kimi、硅基流动、阿里云百炼、豆包、文心一言、智谱开放平台、Ollama、OpenAI 兼容、AIHubMix、Gitee 模力方舟、MiniMax、腾讯云 TokenHub 和 Xiaomi MiMo -- **决策模型**:提供供应商中立的概率判断、单选、评分 API 和决策工作台,支持通过 TypeSafe 或 AIHubMix 使用 Jev +- **决策模型**:提供供应商中立的概率判断、单选、评分 API 和决策工作台 - **统一模型管理**:在 Halo 控制台管理 Provider、API Key Secret、语言 / Embedding / Rerank / 图像生成模型、Adapter 与模型能力 - **模型自动发现**:从 Provider 拉取模型,并保留远程声明、内建能力和管理员覆盖的来源信息 - **统一参数映射**:按 Provider 或模型配置采样、重试、推理、Embedding、Rerank 和图像参数到供应方字段的映射 diff --git a/README_EN.md b/README_EN.md index 5ea97b87..83cf7f73 100644 --- a/README_EN.md +++ b/README_EN.md @@ -10,7 +10,7 @@ ecosystem. ## Features -- **Decision models**: Provider-neutral probability, choice, and score APIs with a decision workbench and Jev access through TypeSafe or AIHubMix. +- **Decision models**: Provider-neutral probability, choice, and score APIs with a decision workbench. - **Multiple AI providers**: Built-in support for OpenAI, OpenRouter, DeepSeek, Moonshot Kimi, SiliconFlow, Alibaba Cloud Model Studio, Doubao, ERNIE, Zhipu AI, Ollama, OpenAI-compatible providers, AIHubMix, Gitee AI, MiniMax, Tencent Cloud TokenHub, and Xiaomi MiMo diff --git a/dev/en/sdk-core/decision-models.md b/dev/en/sdk-core/decision-models.md index ef55e78c..c2c76078 100644 --- a/dev/en/sdk-core/decision-models.md +++ b/dev/en/sdk-core/decision-models.md @@ -3,24 +3,18 @@ [简体中文](../../zh-CN/sdk-core/decision-models.md) | English Decision models answer closed questions about shared text or JSON state. Consumer plugins own thresholds -and subsequent actions. `DecisionModel` is provider-neutral, with built-in TypeSafe and AIHubMix -decision adapters. Other adapters can reuse model resolution, the workbench, and usage recording. +and subsequent actions. `DecisionModel` is provider-neutral. Provider adapters share model resolution, +the workbench, and usage recording. These APIs are new in the development build and require matching plugin and Java/npm SDK versions; the published `1.0.0` API does not include them. -## Configure TypeSafe / Jev +## Configure decision models -Create a **TypeSafe (Jev)** provider and select a Halo Secret for its API key. The default service root -is `https://api.typesafe.ai`; URLs ending in `/v1` also work. Discovery and inference preserve proxy -path prefixes and append `/v1/models` and `/v1/systemone` without duplicating `/v1`. Import a discovered -model or manually enter an accepted provider model ID with model type `decision` and adapter -`typesafe-system-one`. Model discovery uses the account's catalog, not a hardcoded model list. -Optionally configure the default decision model, then use the Console's Decision workbench. - -The adapter supports text/JSON state and `YES_NO`, `CHOICE`, and `SCORE` questions. Choices support -2–255 options; scores support 2–10 ordered levels. Images, Box questions, text generation, tool execution, -and automatic business routing are outside this adapter's current scope. Unsupported native options -are rejected explicitly. +1. Configure a provider that supports decision models and select a Halo Secret for its API key. +2. Import a decision model from provider discovery, or add one manually with model type `decision` + and a matching adapter. +3. Optionally configure the default decision model. In the Console's Decision workbench, enter text + or JSON and add questions. ## Java @@ -51,8 +45,8 @@ also accepts an optional `legend`. These fields are independent and may be absen diagnostics are never synthesized. When present, a distribution must cover every option and sum to one, and a legend must cover every score level. -Confidence is a provider-reported value in `[0, 1]` whose meaning depends on the provider. TypeSafe -uses distribution concentration; this is not a universal confidence definition or a correctness guarantee. +Confidence is a provider-reported value in `[0, 1]` whose meaning depends on the provider; +it is not a universal measure of answer correctness. YES_NO has no separate confidence. Unknown token counts remain null. The response reports the served model. The runtime validates request fields, provider limits, response question IDs/types, and any supplied diagnostics. @@ -75,9 +69,7 @@ request and result types. Label-only and score-only providers do not need to fab or change the shared inference runtime. A boolean-only classifier can use a two-option `CHOICE`; `YES_NO` retains its probability contract rather than converting booleans to invented probabilities. -Keep protocol-required fields in the adapter's validation. The TypeSafe client still requires its -distributions, confidence, and score legends; the optional public contract does not hide a malformed -TypeSafe response. +Validate provider-required protocol fields in the adapter. ## Browser SDK @@ -105,7 +97,3 @@ Existing APIs and persisted values are preserved, and new AiModelService methods Older releases/clients cannot recognize the new model and adapter enum values: update consuming SDKs, and remove decision resources/default slots before downgrading. Automated tests cover the protocol, runtime and workbench; evaluate Chinese/domain-specific accuracy and thresholds with your own data. - -References: [System One](https://docs.typesafe.ai/api), [model discovery](https://docs.typesafe.ai/api/list-models), -[confidence](https://docs.typesafe.ai/confidence), [AIHubMix Jev API](https://aihubmix.com/model/jev-latest/llms.txt), -[AIHubMix model catalog](https://docs.aihubmix.com/cn/api/Models-API). diff --git a/dev/zh-CN/sdk-core/decision-models.md b/dev/zh-CN/sdk-core/decision-models.md index 788de34e..68746eb4 100644 --- a/dev/zh-CN/sdk-core/decision-models.md +++ b/dev/zh-CN/sdk-core/decision-models.md @@ -3,22 +3,15 @@ 简体中文 | [English](../../en/sdk-core/decision-models.md) 决策模型针对共享内容回答封闭问题,返回概率判断、候选选择或有序评分。业务插件决定阈值和后续动作。 -`DecisionModel` 是供应商中立的公共契约,内置 TypeSafe 与 AIHubMix 决策适配器;其他提供商可以通过独立 -适配器接入相同的模型解析、工作台和用量统计。 +`DecisionModel` 是供应商中立的公共契约,各提供商通过适配器接入相同的模型解析、工作台和用量统计。 这些 API 是本次开发新增能力,使用时需要包含此功能的 AI Foundation 插件和 Java/npm SDK;已发布的 `1.0.0` API 不包含它们。 -## 配置 TypeSafe / Jev +## 配置决策模型 -1. 在提供商管理中选择 **TypeSafe (Jev)**,通过 Halo Secret 配置 API Key。 -2. 默认 Base URL 为 `https://api.typesafe.ai`(服务根地址),也支持以 `/v1` 结尾的地址。可配置代理;模型发现与决策调用都会保留路径前缀,且不会重复追加 `/v1`。 -3. 从提供商发现并导入模型,或手动填写供应方允许的模型 ID。模型类型选择 `decision`,适配器为 - `typesafe-system-one`。模型列表来自 `GET /v1/models`,没有硬编码版本列表。 -4. 可在默认模型页设置默认决策模型,在工作台的“决策”页输入文本或 JSON,并添加多个问题。 - -当前适配器支持文本与 JSON 状态,以及 `YES_NO`、`CHOICE`、`SCORE`。每个单选问题最多 255 个选项, -评分问题为 2–10 个有序等级。不提供图片/Box、文本生成、工具执行或自动业务路由。 -原生参数不支持时会明确拒绝,不会静默忽略。 +1. 在提供商管理中配置支持决策模型的提供商,通过 Halo Secret 配置 API Key。 +2. 从提供商发现并导入决策模型,或手动添加模型,将类型设为 `decision` 并选择匹配的适配器。 +3. 可在默认模型页设置默认决策模型,在工作台的“决策”页输入文本或 JSON,并添加多个问题。 ## Java 调用 @@ -48,14 +41,14 @@ return extensionGetter.getEnabledExtension(AiModelService.class) `modelName` 为 `AiModel.metadata.name`;无参或空白名称使用默认决策模型。跨插件仍通过 `ExtensionGetter` 获取服务。`metadata` 仅供本地审计,不会发送给提供商。 -- `YES_NO` 的 `probability` 是“是”的概率,不会自动转为布尔值。TypeSafe 的 `noul` 在适配器内部转换。 +- `YES_NO` 的 `probability` 是“是”的概率,不会自动转为布尔值。 - `CHOICE` 必须返回请求中的一个 `choice`;`probabilities` 和 `confidence` 各自可选。 - `SCORE` 必须返回 `[0, 等级数 - 1]` 内的 `score`,允许小数,不要求由概率加权计算。 等级 `legend`、`probabilities` 和 `confidence` 各自可选。 - 附加字段缺失或为 `null` 时保持缺失,不填零、不制造分布,也不从分数反推置信度。 如果提供概率分布,必须包含全部选项且总和为 1;如果提供图例,必须覆盖全部评分等级。 - `confidence` 是提供商返回的 `[0, 1]` 数值,含义取决于提供商,不能统一解释为答案正确率。 - TypeSafe 的置信度描述分布集中程度;其他提供商可以采用不同定义。`YES_NO` 没有单独的置信度字段。 + `YES_NO` 没有单独的置信度字段。 - `response.model` 保留实际服务模型,token 数量未知时保持 `null`,不能按零用量处理。 - 请求验证先于推理;返回结果的问题 ID、类型、选项、数值范围和概率分布也会校验。 @@ -76,8 +69,7 @@ return extensionGetter.getEnabledExtension(AiModelService.class) 诊断数据,也无需修改公共推理流程。二元分类若只返回布尔结果,可用两个选项的 `CHOICE` 表达; `YES_NO` 保留“是”的概率契约,不把布尔值伪装成概率。 -提供商协议的强制字段在适配器内校验。例如 TypeSafe 本身要求返回的分布、置信度和评分图例仍由 -TypeSafe 客户端严格解析;公共层放宽可选字段不会掩盖它的异常响应。 +提供商协议的强制字段由适配器校验。 ## 浏览器调用自己的插件接口 @@ -109,11 +101,7 @@ if (answer?.type === 'YES_NO') { 原有语言、嵌入、重排和图像 API 及持久化值保持不变。新服务方法提供默认实现,旧的 `AiModelService` 实现可继续加载;新功能仍要求匹配版本的运行时。 -新增 `decision` 模型类型、`typesafe-system-one` / `aihubmix-system-one` 适配器及默认决策槽位后,旧版插件/生成客户端不能 +新增 `decision` 模型类型、决策适配器及默认决策槽位后,旧版插件/生成客户端不能 识别新枚举。升级时一起更新相关 SDK;降级前移除新模型资源并清空决策默认槽位。 自动测试覆盖协议、模型解析、验证、取消、超时和工作台。实际中文业务效果与阈值应使用自己的数据评估。 - -协议依据:[System One](https://docs.typesafe.ai/api)、[模型发现](https://docs.typesafe.ai/api/list-models)、 -[概率与置信度](https://docs.typesafe.ai/confidence)、[AIHubMix Jev 接口](https://aihubmix.com/model/jev-latest/llms.txt)、 -[AIHubMix 模型目录](https://docs.aihubmix.com/cn/api/Models-API)。