엄격한 route 식별자 규칙을 유지하면서 공식 caller의 URL-encoded 표시 모델 이름은 별도 bounded 규칙으로 수용해 Gemini ingress의 잘못된 400을 제거한다.
106 lines
8.7 KiB
Markdown
106 lines
8.7 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`
|
|
- 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` 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 또는 공식 CLI의 bounded 표시 label이며 관측·호환성 검증 대상이다. URL-encoded space를 포함한 `Gemini 3.6 Flash` label을 허용하지만 앞뒤 공백, slash, control character는 거부한다. 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을 사용한다. dev 호환 확인에서 사용하는 label은 `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`, `responseMimeType`, 그리고 상호 배타적인 `responseSchema`/`responseJsonSchema`
|
|
- `tools[].functionDeclarations[]`: `name`, `description`, 상호 배타적인 `parameters`/`parametersJsonSchema`, optional 상호 배타적인 `response`/`responseJsonSchema`
|
|
- `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`로 보존한다. `responseMimeType=application/json`은 Chat `response_format`으로 변환하며 schema가 있으면 고정 이름의 strict `json_schema`, 없으면 `json_object`를 사용한다. `text/plain`은 schema를 허용하지 않는다. Gemini 3.6에서 폐기된 `temperature`, `topP`, `topK`는 형식과 범위만 검증하고 Chat upstream에는 전달하지 않는다. 함수명·JSON argument·opaque thought signature는 caller turn 사이에 의미를 바꾸지 않는다. 동의어 schema 필드가 동시에 존재하거나 schema가 JSON object가 아니면 거부한다. 지원하지 않는 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 수동 호출 확인 기준
|
|
|
|
`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":{...}}}`
|
|
|
|
수동 확인에서는 중첩 `result.status=SUCCESS` 한 건과 process exit 0을 성공 terminal로 본다. 구조가 유효한 `result.status=ERROR` 또는 non-zero exit는 실패로 남기며 성공으로 재해석하지 않는다. 확인 기록에는 raw 응답, tool payload, conversation id, credential을 남기지 않고 caller가 제공한 usage만 원래 단위로 요약한다.
|
|
|
|
## 오류
|
|
|
|
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
|
|
- 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로 전환했다.
|
|
|
|
2026-08-12 실호출 보정에서 official agy planner의 structured-output schema field를 Chat `response_format`으로 변환했다.
|