G0~G8 성과·동맹 측정 OS 작업 일괄 고정
8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본 폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을 git 이력으로 고정하는 것이 목적이다. - contracts/routes/services: measurement, outcome_trajectory, rupture_repair, deliberate_practice, calibration_transfer, supervision_research, multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트 - infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행) - apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가 - docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG - scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트 engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
|
|
@ -19,7 +19,6 @@ from typing import Any, AsyncIterator, Iterable, Literal, cast
|
|||
import httpx
|
||||
|
||||
from app.contracts.engine_gateway import (
|
||||
ENGINE_GATEWAY_DEFAULT_MODEL_SENTINEL,
|
||||
ENGINE_PROVIDER_DEFAULTS,
|
||||
ENGINE_REASONING_EFFORTS,
|
||||
EngineCapabilitiesResponse,
|
||||
|
|
@ -29,6 +28,7 @@ from app.contracts.engine_gateway import (
|
|||
ReasoningEffort,
|
||||
normalize_engine_gateway_model,
|
||||
)
|
||||
from app.services.llm_pricing import estimate_reference_cost
|
||||
|
||||
CODEX_DEFAULT_MODEL, CODEX_DEFAULT_EFFORT = ENGINE_PROVIDER_DEFAULTS["codex_cli"]
|
||||
AGY_DEFAULT_MODEL, AGY_DEFAULT_EFFORT = ENGINE_PROVIDER_DEFAULTS["agy_cli"]
|
||||
|
|
@ -580,6 +580,7 @@ async def _generate_codex(
|
|||
text = ""
|
||||
tokens_in = 0
|
||||
tokens_out = 0
|
||||
cached_input_tokens = 0
|
||||
for line in stdout.splitlines():
|
||||
try:
|
||||
event = json.loads(line)
|
||||
|
|
@ -593,16 +594,25 @@ async def _generate_codex(
|
|||
usage = event.get("usage") or {}
|
||||
tokens_in = int(usage.get("input_tokens") or 0)
|
||||
tokens_out = int(usage.get("output_tokens") or 0)
|
||||
cached_input_tokens = int(usage.get("cached_input_tokens") or 0)
|
||||
elif event.get("type") in {"turn.failed", "error"}:
|
||||
raise ProviderError(str(event.get("message") or event))
|
||||
if not text.strip():
|
||||
raise ProviderError("Codex CLI가 최종 응답을 반환하지 않았습니다.")
|
||||
estimate = estimate_reference_cost(
|
||||
provider="codex_cli",
|
||||
model=model,
|
||||
tokens_in=tokens_in,
|
||||
tokens_out=tokens_out,
|
||||
cached_input_tokens=cached_input_tokens,
|
||||
)
|
||||
return ProviderGenerateResult(
|
||||
text=text,
|
||||
model=model,
|
||||
provider="codex_cli",
|
||||
tokens_in=tokens_in,
|
||||
tokens_out=tokens_out,
|
||||
cost_usd=estimate.cost_usd if estimate is not None else 0.0,
|
||||
structured=_structured_or_none(text, req),
|
||||
)
|
||||
|
||||
|
|
@ -610,32 +620,12 @@ async def _generate_codex(
|
|||
async def _generate_agy(
|
||||
req: GenerateRequest, system_prompt: str, user_payload: str
|
||||
) -> ProviderGenerateResult:
|
||||
binary = _binary("AGY_BIN", "agy")
|
||||
if binary is None:
|
||||
raise ProviderError("Agy CLI를 찾을 수 없습니다.")
|
||||
model, effort = await _resolve_selection(req, "agy_cli")
|
||||
prompt = _cli_prompt(system_prompt, user_payload)
|
||||
if os.name == "nt" and len(prompt) > 24_000:
|
||||
raise ProviderError(
|
||||
"Agy CLI 프롬프트가 Windows 명령줄 안전 한도(24,000자)를 초과했습니다."
|
||||
)
|
||||
args = [binary, "--model", model, "--sandbox"]
|
||||
if effort:
|
||||
args += ["--effort", effort]
|
||||
args += ["--print-timeout", f"{int(CLI_TIMEOUT_SECONDS)}s"]
|
||||
# Agy의 --print는 바로 뒤 토큰을 프롬프트로 해석하며 stdin 입력은
|
||||
# 지원하지 않는다. 옵션을 모두 앞에 두고 프롬프트를 마지막에 둔다.
|
||||
args += ["--print", prompt]
|
||||
stdout, _ = await _run_process(args, cwd=str(_cli_runtime_cwd()))
|
||||
text = stdout.strip()
|
||||
if not text:
|
||||
raise ProviderError("Agy CLI가 최종 응답을 반환하지 않았습니다.")
|
||||
return ProviderGenerateResult(
|
||||
text=text,
|
||||
model=model,
|
||||
provider="agy_cli",
|
||||
structured=_structured_or_none(text, req),
|
||||
)
|
||||
# text 출력은 토큰 사용량을 주지 않는다. stream-json의 terminal result를
|
||||
# 동일하게 소비해 generate와 stream 모두 같은 token/cost 계약을 유지한다.
|
||||
async for event in _stream_agy(req, system_prompt, user_payload):
|
||||
if event.type == "done" and event.result is not None:
|
||||
return event.result
|
||||
raise ProviderError("Agy CLI가 최종 응답을 반환하지 않았습니다.")
|
||||
|
||||
|
||||
async def _stream_agy(
|
||||
|
|
@ -680,6 +670,7 @@ async def _stream_agy(
|
|||
final_text = ""
|
||||
tokens_in = 0
|
||||
tokens_out = 0
|
||||
cached_input_tokens = 0
|
||||
result_status = ""
|
||||
try:
|
||||
async with asyncio.timeout(CLI_TIMEOUT_SECONDS):
|
||||
|
|
@ -705,6 +696,11 @@ async def _stream_agy(
|
|||
usage = result.get("usage") or {}
|
||||
tokens_in = int(usage.get("input_tokens") or 0)
|
||||
tokens_out = int(usage.get("output_tokens") or 0)
|
||||
cached_input_tokens = int(
|
||||
usage.get("cache_read_tokens")
|
||||
or usage.get("cached_input_tokens")
|
||||
or 0
|
||||
)
|
||||
returncode = await proc.wait()
|
||||
except TimeoutError as exc:
|
||||
raise ProviderError(
|
||||
|
|
@ -731,6 +727,13 @@ async def _stream_agy(
|
|||
elif not emitted:
|
||||
emitted = resolved_text
|
||||
yield ProviderStreamEvent(type="delta", text=resolved_text)
|
||||
estimate = estimate_reference_cost(
|
||||
provider="agy_cli",
|
||||
model=model,
|
||||
tokens_in=tokens_in,
|
||||
tokens_out=tokens_out,
|
||||
cached_input_tokens=cached_input_tokens,
|
||||
)
|
||||
yield ProviderStreamEvent(
|
||||
type="done",
|
||||
result=ProviderGenerateResult(
|
||||
|
|
@ -739,6 +742,7 @@ async def _stream_agy(
|
|||
provider="agy_cli",
|
||||
tokens_in=tokens_in,
|
||||
tokens_out=tokens_out,
|
||||
cost_usd=estimate.cost_usd if estimate is not None else 0.0,
|
||||
structured=_structured_or_none(resolved_text, req),
|
||||
),
|
||||
)
|
||||
|
|
@ -789,12 +793,22 @@ async def _generate_claude_api(
|
|||
raise ProviderError("Anthropic Messages API가 텍스트 응답을 반환하지 않았습니다.")
|
||||
usage = body.get("usage") or {}
|
||||
inference_geo = body.get("inference_geo")
|
||||
tokens_in = int(usage.get("input_tokens") or 0)
|
||||
tokens_out = int(usage.get("output_tokens") or 0)
|
||||
estimate = estimate_reference_cost(
|
||||
provider="claude_api",
|
||||
model=str(body.get("model") or model),
|
||||
tokens_in=tokens_in,
|
||||
tokens_out=tokens_out,
|
||||
cached_input_tokens=int(usage.get("cache_read_input_tokens") or 0),
|
||||
)
|
||||
return ProviderGenerateResult(
|
||||
text=text,
|
||||
model=str(body.get("model") or model),
|
||||
provider="claude_api",
|
||||
tokens_in=int(usage.get("input_tokens") or 0),
|
||||
tokens_out=int(usage.get("output_tokens") or 0),
|
||||
tokens_in=tokens_in,
|
||||
tokens_out=tokens_out,
|
||||
cost_usd=estimate.cost_usd if estimate is not None else 0.0,
|
||||
inference_geo=str(inference_geo) if inference_geo else None,
|
||||
structured=_structured_or_none(text, req),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue