AIChannel 与 Provider Adapter

用 Downcity Model Protocol 接入上游模型。

AIChannel 只拥有 Federation 到上游模型的执行边界。它接收 AIChannelStreamInput,返回 ReadableStream<ModelStreamEvent>;fallback、计费、Session 与工具执行都不属于 Channel。

OpenAI-compatible 上游

import {
  AIChannel,
  AIService,
  stream_openai_compatible_model,
  type AIChannelStreamInput,
  type AIChannelStreamResult,
} from "@downcity/federation";

class DeepSeekChannel extends AIChannel {
  constructor() {
    super({
      id: "deepseek",
      env: { DEEPSEEK_API_KEY: "DeepSeek API Key" },
      base_url: "https://api.deepseek.com/v1",
    });
  }

  protected async stream(
    input: AIChannelStreamInput,
  ): Promise<AIChannelStreamResult> {
    const api_key = input.env("DEEPSEEK_API_KEY");
    if (!api_key) throw new Error("DEEPSEEK_API_KEY is required");
    return stream_openai_compatible_model(input, {
      api_key,
      base_url: this.base_url ?? "https://api.deepseek.com/v1",
    });
  }
}

const channel = new DeepSeekChannel();
const ai = new AIService();
ai.use(channel.model({
  id: "deepseek-v4-flash",
  upstream_model: "deepseek-chat",
  name: "DeepSeek V4 Flash",
  context_window: 128_000,
}));
federation.use(ai);

stream_openai_compatible_model() 是边界 Adapter:它把 ModelCall 转成 Chat Completions 请求,并把上游 SSE 转成 Downcity 事件。私有协议应实现自己的 Adapter,但保持相同输入输出。

进程内 ModelClient

不经过 Federation 时,可以直接创建同协议模型:

const model = create_openai_compatible_model({
  id: "deepseek-chat",
  upstream_model: "deepseek-chat",
  base_url: "https://api.deepseek.com/v1",
  api_key: process.env.DEEPSEEK_API_KEY!,
});

Reasoning 与 fallback

ai.use(channel.model({
  id: "text-model",
  upstream_model: "vendor-text-model",
  name: "Text Model",
  reasoning: {
    efforts: [{ id: "high", name: "高" }],
    default_effort: "high",
  },
  fallback: [{
    match: (file) => file.media_type.startsWith("image/"),
    model_id: "vision-model",
  }],
}));

AIService 只把最新 user 消息的 file 依次交给规则。选出最终模型后再校验 reasoning;最新 user 保持原样,命中最终模型 fallback 规则的历史文件会降级为 filename、原始 URL 或 MIME type 文本,再交给目标 Channel。

Channel 与模型的 provider_options 只在服务端合并,由对应 Adapter 解释,不进入公开模型协议。

决策模型(System One)

返回带类型判定而非生成文本的模型,实现 decision() 而不是 stream():

import {
  AIChannel,
  AIService,
  type AIChannelActionInput,
  type AIChargedResult,
  type AIDecisionResult,
} from "@downcity/federation";

class JevChannel extends AIChannel {
  constructor() {
    super({ id: "typesafe", env: { TYPESAFE_API_KEY: "TypeSafe API Key" } });
  }

  async decision(input: AIChannelActionInput): Promise<AIChargedResult<AIDecisionResult>> {
    const api_key = input.env("TYPESAFE_API_KEY");
    if (!api_key) throw new Error("TYPESAFE_API_KEY is required");
    return { output: await call_jev(api_key, input.input) };
  }
}

const ai = new AIService();
ai.use(new JevChannel().model({
  id: "jev-1.13",
  upstream_model: "typesafe/jev-1.13",
  name: "Jev",
  pricing: { currency: "USD", unit: "request", rates: { request: 0.0004 } },
}));
federation.use(ai);

Federation 会自动暴露 POST /v1/ai/decision,并在请求到达 Channel 前完成校验。目录中该模型的 model_type 为 decision、modalities 为 ["decision"];由于它不实现 stream(),不会出现在 Agent 的对话模型选择器里。

计量与计费与其它能力走同一条路径:从 Channel 返回 usage 即记录用量,声明 pricing 即按次计费。