iop/agent-contract/outer/gemini-compatible-api.md
toki 39fa1da55b fix(benchmark): 원샷 비교 실행 실패를 해소한다
호출기별 격리 쓰기 계약과 Gemini ingress, 단일 요청 stage 처리를 맞춰 실제 9-cell 비교가 생성물을 남길 수 있게 한다.
2026-08-12 14:37:17 +09:00

107 lines
8.1 KiB
Markdown

# Gemini-Compatible API Contract
## 계약 메타
- id: `iop.gemini-compatible-api`
- boundary: `outer`
- status: active
- 원본 경로:
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/principal.go`
- `apps/edge/internal/openai/chat_handler.go`
- `scripts/agent_benchmark/agy_iop.py`
- `scripts/agent_benchmark/live_iop.py`
- external caller surface: official Antigravity CLI `agy` 1.1.12 Gemini API-key provider
## 읽는 조건
- Gemini Developer API `streamGenerateContent`, `x-goog-api-key`, `GOOGLE_GEMINI_BASE_URL`, `GEMINI_API_KEY`, `agy` API-key provider, Gemini-native tool/function call, 또는 Gemini-native SSE ingress를 구현·검증할 때 읽는다.
- IOP execution preset을 Gemini-native caller에 노출하거나 `agy` benchmark transport를 변경할 때 읽는다.
## 범위
IOP Edge가 외부 Gemini-native caller에게 제공하는 초기 호환 표면은 다음 streaming endpoint다.
```http
POST /gemini/{route-id}/v1beta/models/{caller-model}:streamGenerateContent?alt=sse
Content-Type: application/json
x-goog-api-key: <IOP principal token>
```
- `{route-id}`는 Edge가 인증된 principal에 대해 해석할 direct route 또는 virtual execution-preset id다. URL path segment 하나의 canonical token이어야 한다.
- `{caller-model}`은 caller가 선택한 Gemini 모델 id이며 관측·호환성 검증 대상이다. provider/credential 또는 execution preset 선택 권한은 갖지 않는다.
- 현재 `agy` 호환 표면은 `alt=sse``streamGenerateContent`만 지원한다. `generateContent`, batch, files, cached content, tuning API는 비범위다.
- direct route와 marked single-request preset은 모두 기존 Edge route resolution, managed admission, provider-pool, preset coordinator를 사용한다. 별도 Gemini 전용 우회 dispatch를 만들지 않는다.
## Caller 설정
공식 `agy` 1.1.12 API-key provider는 다음 값으로 실행한다.
- `~/.gemini/antigravity-cli/settings.json`: `modelProvider``gemini`다.
- `GEMINI_API_KEY`: upstream provider key가 아니라 IOP principal token이다.
- `GOOGLE_GEMINI_BASE_URL`: `https://<edge>/gemini/{route-id}`다.
- 사설 dev CA를 사용하는 경우 caller child에는 표준 `SSL_CERT_FILE``NODE_EXTRA_CA_CERTS`만 명시적으로 전달한다.
- `--model`: 공식 CLI가 인식하는 Gemini 모델 label을 사용한다. benchmark의 Gemini 3.6 Flash 호출은 `Gemini 3.6 Flash`다.
- `--effort`는 API-key provider 호출에 전달하지 않는다. 요청된 high effort는 인증된 IOP route/preset의 effective binding으로 검증한다.
`GEMINI_BASE_URL`, `AGY_PROVIDER`, `AGY_OPENAI_BASE_URL`, `AGY_OPENAI_API_KEY`는 이 계약의 transport가 아니다.
## Auth 및 credential 경계
- `x-goog-api-key`는 Gemini ingress에서 IOP caller 인증 헤더다. Edge는 이를 SHA-256 projection match에만 사용하고 raw 값을 log, metric, response, task evidence에 남기지 않는다.
- `Authorization: Bearer`를 함께 보내면 두 token은 constant-time 비교로 같아야 한다. 다르거나 malformed이면 provider dispatch 전에 Gemini 오류 envelope로 `401`을 반환한다.
- managed mode에서 provider credential은 projected slot과 sealed lease에서만 온다. inbound `x-goog-api-key`를 upstream `Authorization` 또는 upstream `x-goog-api-key`로 전달하지 않는다.
- legacy mode에서도 inbound `x-goog-api-key`를 provider credential로 재사용하지 않는다. provider auth가 별도로 필요하면 기존 명시적 legacy provider-auth 계약만 적용한다.
- marked single-request preset은 managed projection과 고정 stage authorization 없이는 fail closed한다.
## 요청 변환
초기 호환 범위는 official `agy` 1.1.12가 보내는 다음 top-level field다.
- `contents[]`: `role`, `parts[].text`, `parts[].functionCall`, `parts[].functionResponse`, optional opaque `thoughtSignature`
- `systemInstruction`: official `agy``role: user``parts[].text`
- `generationConfig`: `candidateCount`, `maxOutputTokens`, `stopSequences`, `temperature`, `topK`, `topP`, `thinkingConfig.includeThoughts`, `thinkingConfig.thinkingBudget`
- `tools[].functionDeclarations[]`: `name`, `description`, `parametersJsonSchema`
- `toolConfig.functionCallingConfig.mode`
Edge는 이를 기존 Chat/preset ingress의 system/user/assistant/tool message, tool schema와 output cap으로 변환한다. `thinkingConfig`는 Gemini OpenAI-compatible upstream이 요구하는 `extra_body.google.thinking_config`로 보존한다. Gemini 3.6에서 폐기된 `temperature`, `topP`, `topK`는 형식과 범위만 검증하고 Chat upstream에는 전달하지 않는다. 함수명·JSON argument·opaque thought signature는 caller turn 사이에 의미를 바꾸지 않는다. 지원하지 않는 content part, duplicate member, 잘못된 role, malformed function payload 또는 둘 이상의 candidate 요청은 provider dispatch 전에 `400 INVALID_ARGUMENT`으로 거부한다.
## SSE 응답
- 성공 stream은 `Content-Type: text/event-stream``data: <Gemini GenerateContentResponse JSON>` frame을 사용한다.
- text delta는 `candidates[0].content.parts[].text`, reasoning delta는 `thought=true`인 part, 완성된 tool call은 `functionCall` part로 투영한다.
- tool-call argument fragment는 Edge가 bounded buffer에서 완성된 JSON object로 검증한 뒤 한 번만 공개한다. malformed·oversize argument는 성공 tool call로 내보내지 않는다.
- OpenAI `stop`, `length`, `tool_calls` terminal은 Gemini `finishReason``STOP`, `MAX_TOKENS`, `STOP`으로 닫는다. stream 종료 뒤 별도 합성 `system idle` event를 만들지 않는다.
- provider-reported usage가 있으면 `usageMetadata.promptTokenCount`, `candidatesTokenCount`, `thoughtsTokenCount`, `cachedContentTokenCount`, `totalTokenCount`의 존재하는 값만 투영한다. 누락 값을 0으로 발명하지 않는다.
- caller disconnect는 기존 request cancellation 경계를 사용하며 이후 frame을 쓰지 않는다.
## 공식 agy stream-json lifecycle
`agy` 1.1.12의 각 JSONL record는 `event` discriminator와 같은 이름의 중첩 payload를 사용한다.
- init: `{"event":"init","init":{...}}`
- step: `{"event":"step_update","step_update":{"state":...,"step_type":...,"usage":{...}}}`
- terminal: `{"event":"result","result":{"status":"SUCCESS","duration_seconds":...,"num_turns":...,"usage":{...}}}`
benchmark adapter는 중첩 payload만 파싱하며 `result.status=SUCCESS` 한 건과 process exit/quiet를 terminal로 인정한다. `response`, `text_delta`, tool payload, conversation id는 durable evidence에 보존하지 않는다. usage는 caller가 제공한 `input_tokens`, `cache_read_tokens`, `output_tokens`, `thinking_tokens`, `total_tokens`만 원래 단위의 count로 기록하고 누락값을 합성하지 않는다.
## 오류
HTTP commit 전 오류는 다음 Gemini envelope 한 건으로 반환한다.
```json
{"error":{"code":400,"message":"request is invalid","status":"INVALID_ARGUMENT"}}
```
- 인증 실패는 `401 UNAUTHENTICATED`, route/요청 검증 실패는 `400 INVALID_ARGUMENT`, runtime/provider 실패는 `502 UNAVAILABLE`의 고정된 caller-safe message를 사용한다.
- stream commit 뒤 오류는 Gemini `error` payload 한 건으로 끝내며 raw provider body, endpoint, route binding, credential/slot/lease id, prompt, tool argument/result를 포함하지 않는다.
## 변경 시 확인할 코드와 테스트
- route/auth: `apps/edge/internal/openai/routes.go`, `apps/edge/internal/openai/principal.go`
- Gemini request/SSE bridge: `apps/edge/internal/openai/gemini_handler.go`, `apps/edge/internal/openai/gemini_bridge.go`, `apps/edge/internal/openai/gemini_types.go`
- Edge regression: `apps/edge/internal/openai/gemini_handler_test.go`, existing Chat/preset/auth tests
- caller adapter: `scripts/agent_benchmark/agy_iop.py`, `scripts/agent_benchmark/agy_iop_test.py`, `scripts/agent_benchmark/live_iop.py`
- live proof: official `agy --output-format stream-json` through the dev Edge route-specific base URL, with direct and execution-preset effective binding evidence
2026-08-12 dev 검증에서 normal/boundary/auth/tool/SSE 회귀 테스트와 공식 `agy` 1.1.12 direct·hybrid 실호출이 통과해 이 계약을 active로 전환했다.