Files
writer-work-flow/packages/llm_gateway/ww_llm_gateway/factory.py
Yaojia Wang 765dbdfbd4 feat: M4 文风 + M5 生成/多provider/Skill + Kimi Code 订阅接入 + 本地联调修复
M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。
M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变);
  网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则;
  前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。
K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径;
  coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。
本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400);
  GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化;
  字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。
门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 10:39:58 +02:00

78 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Provider→适配器工厂ARCH §4.2/§4.3)。
`build_gateway_for_tier`apps/api据 DB `tier_routing` 的 primary + fallback 逐 provider
建适配器进 `adapters` dict——本工厂按 provider 名分派到对应适配器类,构造其真实客户端。
- `anthropic` → `AnthropicAdapter`(懒构建 `AsyncAnthropic`,不在模块顶层硬 import SDK
- `gemini`/`google` → `GeminiAdapter`(懒构建 `google.genai.Client`)。
- `kimi-code` → `KimiCodeAdapter`OAuth access token 当 api_key + coding base + 伪造头)。
- `kimi-code-key` → `KimiCodeKeyAdapter`(静态 Console Key + coding base**无伪造头**)。
- 其余deepseek/kimi/qwen/glm/openai→ `OpenAICompatAdapter(provider, AsyncOpenAI(...))`。
适配器本身仍接注入客户端(测试零联网);本工厂从 api_key 构造**真实**客户端,
故单测只断分派逻辑(按 provider 名选哪个适配器类),不联网。
"""
from __future__ import annotations
from typing import cast
from openai import AsyncOpenAI
from .adapters.anthropic import AnthropicAdapter, AnthropicClient
from .adapters.base import ProviderAdapter
from .adapters.gemini import GeminiAdapter, GeminiClient
from .adapters.kimi_code import KIMI_CODE_PROVIDER, KimiCodeAdapter, build_kimi_code_client
from .adapters.kimi_code_key import (
KIMI_CODE_KEY_PROVIDER,
KimiCodeKeyAdapter,
build_kimi_code_key_client,
)
from .adapters.openai_compat import OpenAICompatAdapter
# 走专用 SDK 适配器的 provider 名(其余一律走 OpenAI 兼容)。
_ANTHROPIC_PROVIDERS = frozenset({"anthropic"})
_GEMINI_PROVIDERS = frozenset({"gemini", "google"})
def _build_anthropic(provider: str, *, api_key: str, base_url: str | None) -> ProviderAdapter:
# 懒 importSDK 仅在真实接线 Anthropic 时才需要(单测不触发此分支)。
from anthropic import AsyncAnthropic
kwargs: dict[str, object] = {"api_key": api_key}
if base_url is not None:
kwargs["base_url"] = base_url
client = AsyncAnthropic(**kwargs) # type: ignore[arg-type]
# `AsyncAnthropic` 满足 `AnthropicClient` Protocolread-only `messages` 不影响鸭子用法);
# cast 跨过 Protocol settable-attr 静态约束(适配器仅读取该属性)。
return AnthropicAdapter(provider, cast("AnthropicClient", client))
def _build_gemini(provider: str, *, api_key: str, base_url: str | None) -> ProviderAdapter:
# 懒 import`google-genai` 仅在真实接线 Gemini 时才需要。
from google import genai
client = genai.Client(api_key=api_key)
# `genai.Client` 满足 `GeminiClient` Protocol用其 `.aio.models`cast 跨过
# Protocol settable-attr 静态约束(适配器仅读取 `.aio`)。
return GeminiAdapter(provider, cast("GeminiClient", client))
def build_adapter(provider: str, *, api_key: str, base_url: str | None = None) -> ProviderAdapter:
"""按 provider 名构造对应适配器,从 api_key+可选 base_url建真实客户端。
`build_gateway_for_tier`apps/api逐 provider 调用本工厂,组装回退链的 adapters。
"""
if provider == KIMI_CODE_PROVIDER:
# Kimi 订阅 planOAuthapi_key = OAuth access tokencoding base + 伪造头(缺头 403
return KimiCodeAdapter(build_kimi_code_client(api_key, base_url=base_url))
if provider == KIMI_CODE_KEY_PROVIDER:
# Kimi 订阅 plan静态 Console KeyToS 合规coding base + 纯 bearer**无伪造头**
# 结构化走 JSON 模式thinking 兼容)。
return KimiCodeKeyAdapter(build_kimi_code_key_client(api_key, base_url=base_url))
if provider in _ANTHROPIC_PROVIDERS:
return _build_anthropic(provider, api_key=api_key, base_url=base_url)
if provider in _GEMINI_PROVIDERS:
return _build_gemini(provider, api_key=api_key, base_url=base_url)
return OpenAICompatAdapter(provider, AsyncOpenAI(api_key=api_key, base_url=base_url))