앱(웹·서버·스크립트 등)에 AI/LLM 을 연결할 때 사용. tiguclaw 에 붙이는 두 방식 — (A) tiguclaw 직결 커스텀 엔드포인트(비서의 도구·스킬·메모리까지 쓰는 에이전틱 호출, 규약 자유) vs (B) OpenAI 호환 LLM 게이트웨이(OpenRouter·OpenAI 와 baseURL 만 바꿔 스왑 가능한 표준 규약) — 의 선택 기준·규약·배선 절차·검증을 담는다. 트리거: '앱에 AI 붙여줘', '이 앱에서 LLM 쓰게 해줘', 'tiguclaw 를 백엔드로', '오픈라우터처럼 연결', 'API 로 비서 호출', '게이트웨이 켜줘', 앱 개발 중 LLM 연결 지점에 도달했을 때. 사용자가 방식을 안 정했으면 §1 기준으로 추천하고 확인받은 뒤 배선한다.
Installs into .claude/skills of the current project.
Are you the author of App Ai Wiring?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/tigu77-app-ai-wiring)
---
name: app-ai-wiring
description: "앱(웹·서버·스크립트 등)에 AI/LLM 을 연결할 때 사용. tiguclaw 에 붙이는 두 방식 — (A) tiguclaw 직결 커스텀 엔드포인트(비서의 도구·스킬·메모리까지 쓰는 에이전틱 호출, 규약 자유) vs (B) OpenAI 호환 LLM 게이트웨이(OpenRouter·OpenAI 와 baseURL 만 바꿔 스왑 가능한 표준 규약) — 의 선택 기준·규약·배선 절차·검증을 담는다. 트리거: '앱에 AI 붙여줘', '이 앱에서 LLM 쓰게 해줘', 'tiguclaw 를 백엔드로', '오픈라우터처럼 연결', 'API 로 비서 호출', '게이트웨이 켜줘', 앱 개발 중 LLM 연결 지점에 도달했을 때. 사용자가 방식을 안 정했으면 §1 기준으로 추천하고 확인받은 뒤 배선한다."
reach: main
---
# 앱 ↔ AI 연결 배선 (tiguclaw)
앱에 AI 를 붙이는 방법은 **두 가지**다. 성격이 완전히 달라 먼저 고르고 배선한다.
**사용자가 지정했으면 그대로 따르고, 안 정했으면 §1 로 추천 + 확인**받는다(맘대로 정하지 말 것).
---
## §1 어느 쪽인가 — 판단 기준
| | **A. tiguclaw 직결 엔드포인트** | **B. OpenAI 호환 게이트웨이** |
|---|---|---|
| 앱이 얻는 것 | **비서 자체**(도구·스킬·메모리·서브에이전트·프로젝트 맥락) | **LLM 한 방**(순수 텍스트 생성) |
| 규약 | **없음** — 내가 자유 설계 | **OpenAI 스펙 고정** |
| 교체 | tiguclaw 전용(스왑 불가) | **OpenRouter·OpenAI 로 baseURL 만 바꿔 스왑** |
| 반영 | 재시작 **불요**(데이터 기반) | 토큰 설정 시 **재시작 필요**(.env) |
**A 를 고르는 신호**: "내 파일 읽어서", "메모리 참고해서", "스킬 태워서", "프로젝트 맥락 알고",
여러 단계 자율 작업, tiguclaw 만의 능력이 핵심일 때.
**B 를 고르는 신호**: 요약·분류·번역·챗봇 등 **모델만 있으면 되는** 일, 나중에 상용 API 로
갈아탈 계획, 이미 OpenAI SDK 로 짠 앱, 함수호출/비전 같은 **표준 기능**이 필요할 때.
> 애매하면 **B**(표준이라 나중에 A 로 못 바꾸는 손해가 없고, 스왑 자유). 단 "비서를 부르고
> 싶은 것"이면 B 로는 절대 안 되니 A.
---
## §2 모드 A — tiguclaw 직결 커스텀 엔드포인트
엔드포인트 = **슬래시 명령의 HTTP 판**. `<home>/endpoints/<name>.md`(frontmatter + 프롬프트
템플릿)를 http-bridge 가 **매 요청 발견**해 서빙한다. 임의 코드 0 = 데이터만, **재시작 불요**.
### 배선
1. `register_endpoint` 도구로 등록(`list_endpoints`/`delete_endpoint` 로 조회·삭제).
- `path` — 라우트(예 `/weather`). 슬래시 시작·소문자 정규화.
- `method` — `GET`|`POST`(기본 POST).
- `role` — 인증 게이트(기본 `write`). 앱은 **bridge 토큰**으로 호출.
- `mode` — 기본 `restricted`(도구 0). 비서 도구가 필요하면 명시적으로 열어야 함.
- 본문 = 프롬프트 템플릿(요청 파라미터를 끼워 넣음).
2. 앱은 그 경로를 호출: `POST http://127.0.0.1:<HTTP_BRIDGE_PORT>/<path>` + bridge 토큰.
### 동기 / 스트리밍
엔드포인트도 **둘 다** 된다 — 앱이 고른다.
- **동기(기본)**: 그냥 POST → `{ "result": "<본문>" }` 한 번에. 앱 코드 2줄.
- **스트리밍**: body 에 `"stream": true`(또는 `?stream=1`) → SSE.
```
data: {"type":"delta","text":"…"} 진행 조각(0회 이상)
data: {"type":"result","result":"…"} 최종(성공 시 1회)
data: {"type":"error","error":"…"} 실패(1회)
data: [DONE] 종료 표식
```
★앱은 **result/error 를 받았는지**로 성패를 판정할 것(연결만 끊긴 것과 구분).
★`EventSource` 는 POST 불가 → `fetch` + `ReadableStream` 으로 읽는다.
- **타임아웃**: 엔드포인트 전용 `ENDPOINT_TIMEOUT_MS`(기본 5분, env 조정). 폴백 낀 긴 턴(70초)이 504로 잘리던 실사고 때문에 분리·상향했다. 스트리밍이면 데이터가
흘러 중간 계층(프록시·Tailscale) idle timeout 도 회피된다.
### 주의
- 기본이 `restricted`(도구 0)라 **에이전틱 능력을 쓰려면 mode 를 열어야** 한다. 열 때는
무인 트리거라는 점을 감안해 위험 도구 노출을 최소화(사용자 확인).
- 응답 규약은 내가 정한다 = 앱과 **계약을 명시**해 둘 것(문서/주석).
---
## §3 모드 B — OpenAI 호환 LLM 게이트웨이 (규약)
`POST /v1/chat/completions` · `GET /v1/models` 를 OpenAI 규격 그대로 서빙한다.
**OpenRouter 와 같은 규약**이라 앱은 `baseURL`·`apiKey`만 바꾸면 그대로 붙는다.
### 켜는 법 (기본은 꺼져 있음 = 404)
**① 토큰(시크릿) — `<home>/.env`, 최초 1회만 · 재시작 필요**
```
LLM_GATEWAY_TOKEN=<임의 비밀문자열>
```
시크릿이라 **파일(settings.json)에 raw 로 두지 않는다**. env 는 부팅 고정이라
이 값을 새로 넣거나 바꿀 때만 재시작(`tiguclaw restart`).
**② 나머지 설정 — `<home>/settings.json`, ★재시작 불요(매 요청 fresh read)**
```json
{
"gateway": {
"enabled": true,
"models": ["codex:gpt-5.5"],
"maxConcurrency": 4,
"tokenEnv": "LLM_GATEWAY_TOKEN"
}
}
```
- `enabled` — **킬스위치**(토큰은 둔 채 껐다 켰다). 생략=true.
- `models` — 기본 모델 풀. 생략 시 env `LLM_GATEWAY_MODELS`, 그것도 없으면 기본 모델 프로파일.
- `maxConcurrency` — 동시 처리 상한(초과 429). 생략=4.
- `tokenEnv` — 토큰을 읽을 env 변수명. 생략=`LLM_GATEWAY_TOKEN`.
- ★`gateway` 섹션 자체가 없으면 **레거시 env 경로**(토큰 존재만으로 활성).
→ **토큰만 한 번 심어두면, 그 뒤 켜기/끄기·모델·동시성 변경은 재시작 없이 즉시 반영된다.**
즉 비서가 사용자 대화를 끊지 않고 게이트웨이를 토글할 수 있다.
주소 = `http://127.0.0.1:<HTTP_BRIDGE_PORT>/v1` (기본 127.0.0.1 바인드 = 로컬 전용).
### 앱 배선 (OpenAI SDK — OpenRouter 자리에 그대로)
```js
const client = new OpenAI({
baseURL: process.env.OPENAI_BASE_URL, // http://127.0.0.1:<port>/v1
apiKey: process.env.OPENAI_API_KEY, // = LLM_GATEWAY_TOKEN
});
```
### 모델 이름 규약 (★OpenRouter 슬러그와 다름)
- `provider:model` — `anthropic:claude-sonnet-5`, `codex:gpt-5.6-terra`, `openai:gpt-4o-mini`, `ollama:qwen2.5:7b`(settings.json `models.providers` 에 정의한 이름)
- `tier:<프로파일>` — settings.json `models.profiles` 의 명명 프로파일(`tier:high`·`tier:default` 등)
- 목록은 `GET /v1/models` 로 확인(거기 나온 id 를 그대로 쓰면 왕복 보장)
- ⚠️ **목록에 없는 이름을 보내면 400 이 아니라 조용히 기본 풀로 폴백**한다(알려진 갭) —
반드시 `/v1/models` 의 id 를 쓸 것.
### 지원 범위
**되는 것**: 채팅 · 스트리밍(SSE, `stream:true` — **OpenAI SDK 가 파싱까지 처리**하므로 앱
부담 거의 0. 엔드포인트 스트리밍은 직접 파싱해야 하는 것과 대비되는 장점) ·
**함수호출(tools/tool_calls)** · **비전(이미지 입력)** · `usage` 토큰 · `/v1/models`
- 함수호출: 앱이 `tools` 를 주면 모델이 **실행하지 않고** `tool_calls` 를 반환
(`finish_reason:"tool_calls"`) → **앱이 실행**하고 `role:"tool"` 로 결과를 돌려주면 이어감.
★**세 어댑터 전부**(claude·codex·openai)에서 되고, **구독 인증만으로도 된다**(API 키 불요).
200 으로 받아 "모델이 도구를 안 쓴다"로만 보였다. 그때 배운 것: **되는지 물어보지 말고
§5 검증을 돌려라.**
- `tool_choice` — `"none"` 은 스키마를 아예 안 넘겨 집행하고, `"required"` 를 못 지키면
**에러**(`tool_choice_unsatisfied`)로 알린다. 텍스트를 성공인 척 주지 않는다.
- ★**빈 응답은 없다.** 텍스트도 함수콜도 없으면 200 이 아니라 `empty_completion` 에러다
(응답은 셋 중 하나: `tool_calls` · 텍스트 · 명시적 에러).
- 비전: `image_url` 의 **`data:` URI(base64)만** 지원. `http(s)` URL 은 미지원(SSRF 방지).
**안 되는 것**: 이미지·음성 **생성**(`/v1/images`·`/v1/audio`) · `/v1/completions`(legacy) ·
embeddings · `response_format` 스키마 강제.
→ 이미지/음성 생성이 필요하면 **그 모달리티만 실제 프로바이더**(OpenAI·Replicate 등)로 직접 붙인다.
### 멀티라운드 한계(정직하게)
게이트웨이는 요청마다 새 세션이라, 이전 `tool_calls`/`role:"tool"` 은 **텍스트로 재구성**해
모델에 전달한다(네이티브 tool-state 재현이 아님). 단순 왕복엔 충분하나, 복잡한 다단계 함수
호출 루프는 기대만큼 안 이어질 수 있다.
---
## §4 dev ↔ prod 스위처블 배선 (기본 권장)
두 모드 다 OpenAI 호환이라 **코드 수정 0**으로 전환된다. 앱엔 항상 env 두 벌을 준비:
```bash
# dev — tiguclaw 게이트웨이(무료·로컬)
OPENAI_BASE_URL=http://127.0.0.1:7011/v1 # HTTP_BRIDGE_PORT (기본 7011)
OPENAI_API_KEY=<LLM_GATEWAY_TOKEN>
OPENAI_MODEL=codex:gpt-5.5
# prod — 상용 API / OpenRouter
OPENAI_BASE_URL=https://openrouter.ai/api/v1
OPENAI_API_KEY=sk-or-...
OPENAI_MODEL=anthropic/claude-sonnet-4.6
```
모델 이름이 규약별로 다르므로 **모델명도 env 로** 뽑아둘 것.
### 비서 인격·기억은 실리지 않는다
게이트웨이 턴은 **격리 작업폴더**에서 돌아 tiguclaw 의 `AGENT.md`·`SYSTEM.md`·메모리가 안
실린다. 앱이 준 `system` 이 곧 시스템 프롬프트다(안 주면 빈 값).
- ★잔여(막을 수 없음): **구독 OAuth 계정 이메일**은 SDK 가 스스로 싣는다(`~/.claude.json`
`oauthAccount`). 우리 프롬프트·설정엔 없다. 완전 차단은 상용 API 키 경로뿐 — 아래 약관
가드와 같은 결론으로 이어진다.
### ★약관 가드 (중요)
tiguclaw 는 **개인 구독 인증**(Claude Code OAuth·ChatGPT)으로 돌 수 있다. 이 백엔드를
**외부 앱·타인에게 중계·판매하면 약관 위반 소지**가 크다.
- **본인 로컬 dev/테스트** → 게이트웨이 OK
- **외부 배포·타인 사용·부하 테스트** → 반드시 **상용 API 키**(`LLM_GATEWAY_MODELS` 를 상용
프로바이더로 두거나, 앱을 OpenRouter/OpenAI 로 직결)
### 네트워크 노출 시
기본 `127.0.0.1`. 다른 기기에서 붙이려면 `HTTP_BRIDGE_HOST=0.0.0.0` — 단 **TLS 없음**이라
Tailscale 같은 암호화 경로 권장, 그리고 그 포트엔 대시보드·제어 API 도 함께 열리므로
프록시로 `/v1/*` 만 통과시키는 게 안전.
★**이름으로 붙이면 `HTTP_BRIDGE_ALLOWED_HOSTS` 도 필요하다** (DNS 리바인딩 방어). IP 로
붙으면 설정 불요 — 리바인딩은 이름이 있어야 성립하므로 IP 리터럴은 그냥 통과한다.
안 적어서 막히면 403 본문이 적을 값을 알려준다.
---
## §5 배선 후 검증 (반드시)
```bash
# 게이트웨이(모드 B)
curl -H "Authorization: Bearer <TOKEN>" http://127.0.0.1:<port>/v1/models
curl http://127.0.0.1:<port>/v1/chat/completions \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{"model":"<위 목록의 id>","messages":[{"role":"user","content":"안녕"}]}'
```
- 함수호출을 쓰는 앱이면 `tools` 를 실어 `finish_reason:"tool_calls"` 가 오는지까지 확인.
- 모드 A 면 등록한 경로를 bridge 토큰으로 호출해 실제 응답을 확인.
- **앱에서 한 번 실제로 호출**해 보고 보고할 것(추측 금지).
---
## 불변식
- 사용자가 모드를 정했으면 **그대로**. 안 정했으면 §1 로 **추천 후 확인**.
- 게이트웨이 토큰·상용 키는 **절대 코드·settings.json 에 raw 로 두지 말 것**(env 만).
- **토큰을 새로 심을 때만** 재시작이 필요하다 — 라이브 데몬이 대화 중일 수 있으니 **사전 고지**.
그 외(켜기/끄기·모델·동시성)는 settings.json 편집으로 재시작 없이 처리한다.