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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Halo 官方 AI 能力平台,统一接入主流大模型,为插件生态提
## 功能特性

- **多提供商支持**:内置 OpenAI、OpenRouter、DeepSeek、月之暗面 Kimi、硅基流动、阿里云百炼、豆包、文心一言、智谱开放平台、Ollama、OpenAI 兼容、AIHubMix、Gitee 模力方舟、MiniMax、腾讯云 TokenHub 和 Xiaomi MiMo
- **决策模型**:提供供应商中立的概率判断、单选、评分 API 和决策工作台
- **统一模型管理**:在 Halo 控制台管理 Provider、API Key Secret、语言 / Embedding / Rerank / 图像生成模型、Adapter 与模型能力
- **模型自动发现**:从 Provider 拉取模型,并保留远程声明、内建能力和管理员覆盖的来源信息
- **统一参数映射**:按 Provider 或模型配置采样、重试、推理、Embedding、Rerank 和图像参数到供应方字段的映射
Expand Down
1 change: 1 addition & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ ecosystem.

## Features

- **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
Expand Down
232 changes: 219 additions & 13 deletions api-docs/openapi/v3_0/aiFoundationApis.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions api/src/main/java/run/halo/aifoundation/AiModelService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,6 +18,19 @@
*/
public interface AiModelService extends ExtensionPoint {

/** Resolves the configured default decision model. */
default Mono<DecisionModel> 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> decisionModel(String modelName) {
return Mono.error(new UnsupportedOperationException("Decision models are not supported"));
}

/**
* Resolves the configured default language model.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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 values mean unknown.
* Output flags indicate supported information, not a guarantee that every response includes it.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DecisionCapability {
private List<DecisionQuestionType> questionTypes;
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<DecisionQuestionType> questionTypes, Boolean structuredState,
Integer maxChoiceOptions, Integer maxScoreLevels) {
this(questionTypes, structuredState, maxChoiceOptions, maxScoreLevels,
null, null, null, null, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()}.
*/
Expand All @@ -39,6 +46,7 @@ public CapabilitySource source(CapabilityDomain domain) {
return null;
}
return switch (domain) {
case DECISION -> decision;
case LANGUAGE -> language;
case IMAGE_GENERATION -> imageGeneration;
};
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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 level index in [0, number of levels - 1], which may be fractional. */
private Double score;
/** Optional complete distribution over option identifiers or zero-based score level indices. */
private Map<String, Double> probabilities;
/** Optional provider-reported CHOICE/SCORE confidence in [0, 1]; semantics vary by provider. */
private Double confidence;
/** Optional SCORE level indices and descriptions. */
private Map<String, String> legend;
}
Original file line number Diff line number Diff line change
@@ -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<DecisionResponse> decide(DecisionRequest request);

/** Read-only capability snapshot, or null when unavailable. */
default DecisionCapability capabilities() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> options;
/** SCORE descriptions in ascending order; the first level has index zero. */
private List<String> 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<String, String> options) {
return builder().type(DecisionQuestionType.CHOICE).instructions(instructions)
.options(options).build();
}

public static DecisionQuestion score(String instructions, List<String> levels) {
return builder().type(DecisionQuestionType.SCORE).instructions(instructions)
.levels(levels).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package run.halo.aifoundation.decision;

/** Provider-neutral kinds of closed questions. */
public enum DecisionQuestionType {
YES_NO,
CHOICE,
SCORE
}
Original file line number Diff line number Diff line change
@@ -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<String, DecisionQuestion> questions;
/** Local audit metadata, never sent to the provider. */
private Map<String, Object> metadata;
private transient CancellationToken cancellationToken;
private transient GenerationTimeouts timeouts;

@Transient
public CancellationToken getCancellationToken() {
return cancellationToken;
}

@Transient
public GenerationTimeouts getTimeouts() {
return timeouts;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, DecisionAnswer> answers;
private DecisionUsage usage;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ haloPlugin {
}

halo {
version = '2.26'
version = '2.26.0'
}

def generatedOpenApi = layout.projectDirectory.file(
Expand Down
Loading
Loading