✨ feat: WP-05 模型网关核心——逻辑模型、流式适配与 Run 真实执行
- 迁移 7+8:logical_model(能力目录)、provider_endpoint(密钥只存环境变量名,永不落库)、turn、model_call(每 attempt 单独持久化:usage/TTFT/错误,Provider 未返回保持 null 不估算——ADR-003);message.run_id 关联 - OpenAI-compatible 流式适配器:httpx SSE 解析、标准化 StreamChunk(delta/usage/done)、transport 可注入(测试与故障注入);密钥经环境变量读取不进日志 - Run 执行器:后台执行完整生命周期——running → content.block.started → 真实 content.block.delta*(流式产生,delta_index 单调)→ content.block.completed → message.completed → 终态;assistant 消息为权威快照;ModelCall 记录 usage 与 TTFT;无可用端点时 Run 显式失败并留 error 事件(fail-closed) - post_message:响应提交后由框架 BackgroundTasks 执行 Run(显式预提交保证 Run 落库);SSE 改为增量轮询(重放 + 等待新事件至终态,30s 上限) - 测试 54 项全绿(+2):端到端 mock 流验证 ModelCall usage/TTFT/assistant 权威消息、无端点 Run failed + content.block.failed 事件
This commit is contained in:
1 parent
40221170ae
commit
b4242f3844
10 files changed
+789
-56
No files matched your search
@@ -0,0 +1,94 @@
|
||||
"""模型网关:逻辑模型、端点、Turn 与 ModelCall
|
||||
|
||||
Revision ID: 0fb7d605080a
|
||||
Revises: 6292e21950cc
|
||||
Create Date: 2026-09-11 09:22:23.431933
|
||||
|
||||
"""
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0fb7d605080a'
|
||||
down_revision: str | Sequence[str] | None = '6292e21950cc'
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('logical_model',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('capabilities', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('description', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_logical_model'))
|
||||
)
|
||||
op.create_table('provider_endpoint',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('logical_model_id', sa.String(), nullable=False),
|
||||
sa.Column('provider', sa.String(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('base_url', sa.String(), nullable=False),
|
||||
sa.Column('model_id', sa.String(), nullable=False),
|
||||
sa.Column('api_key_env', sa.String(), nullable=False),
|
||||
sa.Column('priority', sa.Integer(), nullable=False),
|
||||
sa.Column('enabled', sa.Boolean(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.CheckConstraint("provider IN ('openai_compatible', 'anthropic')", name=op.f('ck_provider_endpoint_provider_values')),
|
||||
sa.ForeignKeyConstraint(['logical_model_id'], ['logical_model.id'], name=op.f('fk_provider_endpoint_logical_model'), ondelete='cascade'),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_provider_endpoint'))
|
||||
)
|
||||
op.create_index(op.f('ix_provider_endpoint_logical_model_id'), 'provider_endpoint', ['logical_model_id'], unique=False)
|
||||
op.create_table('turn',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('run_id', sa.String(), nullable=False),
|
||||
sa.Column('index', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['run_id'], ['run.id'], name=op.f('fk_turn_run'), ondelete='cascade'),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_turn'))
|
||||
)
|
||||
op.create_index(op.f('ix_turn_run_id'), 'turn', ['run_id'], unique=False)
|
||||
op.create_index('uq_turn_run_index', 'turn', ['run_id', 'index'], unique=True)
|
||||
op.create_table('model_call',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('turn_id', sa.String(), nullable=False),
|
||||
sa.Column('endpoint_id', sa.String(), nullable=True),
|
||||
sa.Column('attempt', sa.Integer(), nullable=False),
|
||||
sa.Column('status', sa.String(), nullable=False),
|
||||
sa.Column('request_summary', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('usage', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
||||
sa.Column('ttft_ms', sa.Integer(), nullable=True),
|
||||
sa.Column('error', sa.String(), nullable=True),
|
||||
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.CheckConstraint("status IN ('pending', 'streaming', 'succeeded', 'failed')", name=op.f('ck_model_call_status_values')),
|
||||
sa.ForeignKeyConstraint(['endpoint_id'], ['provider_endpoint.id'], name=op.f('fk_model_call_provider_endpoint'), ondelete='set null'),
|
||||
sa.ForeignKeyConstraint(['turn_id'], ['turn.id'], name=op.f('fk_model_call_turn'), ondelete='cascade'),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_model_call'))
|
||||
)
|
||||
op.create_index(op.f('ix_model_call_turn_id'), 'model_call', ['turn_id'], unique=False)
|
||||
op.create_index('uq_model_call_turn_attempt', 'model_call', ['turn_id', 'attempt'], unique=True)
|
||||
op.create_foreign_key(op.f('fk_character_character_version'), 'character', 'character_version', ['current_version_id'], ['id'], ondelete='set null', use_alter=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(op.f('fk_character_character_version'), 'character', type_='foreignkey')
|
||||
op.drop_index('uq_model_call_turn_attempt', table_name='model_call')
|
||||
op.drop_index(op.f('ix_model_call_turn_id'), table_name='model_call')
|
||||
op.drop_table('model_call')
|
||||
op.drop_index('uq_turn_run_index', table_name='turn')
|
||||
op.drop_index(op.f('ix_turn_run_id'), table_name='turn')
|
||||
op.drop_table('turn')
|
||||
op.drop_index(op.f('ix_provider_endpoint_logical_model_id'), table_name='provider_endpoint')
|
||||
op.drop_table('provider_endpoint')
|
||||
op.drop_table('logical_model')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,36 @@
|
||||
"""消息关联 Run
|
||||
|
||||
Revision ID: 1c1f7edb7cbe
|
||||
Revises: 0fb7d605080a
|
||||
Create Date: 2026-09-11 09:24:35.912343
|
||||
|
||||
"""
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '1c1f7edb7cbe'
|
||||
down_revision: str | Sequence[str] | None = '0fb7d605080a'
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('message', sa.Column('run_id', sa.String(), nullable=True))
|
||||
op.create_index(op.f('ix_message_run_id'), 'message', ['run_id'], unique=False)
|
||||
op.create_foreign_key(op.f('fk_message_run'), 'message', 'run', ['run_id'], ['id'], ondelete='set null')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(op.f('fk_message_run'), 'message', type_='foreignkey')
|
||||
op.drop_index(op.f('ix_message_run_id'), table_name='message')
|
||||
op.drop_column('message', 'run_id')
|
||||
# ### end Alembic commands ###
|
||||
Reference in new issue
Block a user