iop/docs/dev-opencode-settings-guide.md
toki a89b42ed14 feat(agent-ops): GLM 백업 경로를 OpenCode로 전환한다
Gemini 쿼터 소진 시 reasoning 단계가 한 단계 높은 GLM-5.2를 선택하고 실행 상태에도 effort를 보존하기 위해 변경한다.
2026-08-05 05:05:55 +09:00

13 KiB

dev OpenCode Settings Guide

IOP dev OpenAI-compatible endpoint를 OpenCode에서 사용하기 위한 설정과 복구 절차다. 이 문서는 저장소 코드나 Edge 런타임을 수정하는 절차가 아니라 사용자 호스트의 OpenCode 설정만 다룬다.

2026-08-05 기준 다음 조합을 실제 호출로 확인했다.

  • OpenCode: 1.18.3
  • IOP endpoint: http://toki-labs.com:18083/v1
  • local model: iop/ornith:35b
  • cloud model: iop-glm/glm-5.2

인증 경계

IOP를 OpenCode에 연결할 때 서로 다른 두 토큰을 구분한다.

용도 요청 위치 OpenCode 환경 변수
IOP 접속 토큰 Authorization: Bearer ... IOP_OPENCODE_API_KEY
upstream provider 토큰 X-IOP-Provider-Authorization: ... IOP_GLM_CODING_PLAN_TOKEN

/v1/models부터 실패하면 IOP 접속 토큰 문제다. /v1/models는 성공하지만 completion에서 provider auth token required가 나오면 upstream provider header 문제다. Edge 설정의 legacy openai.bearer_token 값을 사용자용 IOP 토큰 대신 복사하지 않는다.

실제 토큰은 tracked 문서, 저장소 설정, 셸 명령 인자, 로그, 채팅에 남기지 않는다.

원격 SOPS 토큰 찾기

dev 사용자 토큰의 현재 source of truth는 원격 호스트 toki@toki-labs.com의 다음 파일이다.

~/.config/iop/secrets/dev-openai-toki.sops.yaml

이 파일은 원격 호스트 자체의 파일이다. code-server 컨테이너의 /config/.config/sops/iop/*.sops.yaml과 혼동하지 않는다.

현재 OpenCode에는 IDE agent 용도로 발급된 다음 token entry를 사용한다.

tokens.toki-dev-cline

원격 macOS에서는 SOPS와 age key 경로를 명시한다.

export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"
secret_file="$HOME/.config/iop/secrets/dev-openai-toki.sops.yaml"

/opt/homebrew/bin/sops -d --output-type json "$secret_file" |
  jq -r 'paths(scalars) | map(tostring) | join(".")'

위 명령은 key path만 출력한다. 복호화된 전체 JSON이나 token value를 터미널에 출력하지 않는다.

원격 endpoint와 token 유효성은 응답 본문이나 token을 출력하지 않고 상태 코드로 확인한다.

export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"
secret_file="$HOME/.config/iop/secrets/dev-openai-toki.sops.yaml"
base_url="$(/opt/homebrew/bin/sops -d --extract '["base_url"]' "$secret_file")"
iop_token="$(/opt/homebrew/bin/sops -d --extract '["tokens"]["toki-dev-cline"]' "$secret_file")"

http_code="$(
  curl -sS -o /dev/null -w '%{http_code}' \
    -H "Authorization: Bearer $iop_token" \
    "$base_url/v1/models"
)"
printf 'status=%s token_present=%s\n' "$http_code" "$([[ -n "$iop_token" ]] && echo yes || echo no)"
unset iop_token

정상 기준은 status=200이다. SOPS의 base_url은 원격 호스트 내부에서 사용하는 127.0.0.1 주소일 수 있으므로, 로컬 OpenCode 설정에는 외부에서 접근 가능한 dev endpoint를 사용한다.

로컬 SOPS로 안전하게 이전

원격 SOPS 파일의 age recipient와 로컬 age key가 다르면 파일 자체를 복사해도 복호화할 수 없다. token을 평문 임시 파일에 저장하지 않고 SSH pipe에서 바로 로컬 recipient로 재암호화한다.

로컬 기준 경로:

~/.config/sops/age/keys.txt
~/.config/sops/iop/iop-opencode.sops.yaml
local_recipient="$(
  "$HOME/.local/bin/age-keygen" -y "$HOME/.config/sops/age/keys.txt"
)"

umask 077
ssh toki@toki-labs.com \
  "export SOPS_AGE_KEY_FILE=/Users/toki/.config/sops/age/keys.txt; \
   /opt/homebrew/bin/sops -d \
     --extract '[\"tokens\"][\"toki-dev-cline\"]' \
     /Users/toki/.config/iop/secrets/dev-openai-toki.sops.yaml" |
  jq -Rn '{data: input}' |
  "$HOME/.local/bin/sops" encrypt \
    --age "$local_recipient" \
    --input-type json \
    --output-type yaml \
    --output "$HOME/.config/sops/iop/iop-opencode.sops.yaml" \
    /dev/stdin

chmod 600 "$HOME/.config/sops/iop/iop-opencode.sops.yaml"
unset local_recipient

복호화 가능 여부는 값 대신 길이와 endpoint 상태만 확인한다.

iop_token="$(
  "$HOME/.local/bin/sops" -d --extract '["data"]' \
    "$HOME/.config/sops/iop/iop-opencode.sops.yaml"
)"

http_code="$(
  curl -sS -o /dev/null -w '%{http_code}' \
    -H "Authorization: Bearer $iop_token" \
    http://toki-labs.com:18083/v1/models
)"
printf 'status=%s token_length=%s\n' "$http_code" "${#iop_token}"
unset iop_token

OpenCode 설정 파일

현재 구성은 다음 파일을 사용한다.

~/.config/opencode/opencode.json
~/.config/opencode/iop-glm.json
~/.config/opencode/plugins/iop-quota-guard.js
~/.local/bin/opencode
~/.local/bin/opencode-tmp-wrapper

opencode.json은 기본 IOP/Ornith provider를, iop-glm.json은 GLM provider와 build/plan agent override를 정의한다. 실행 wrapper가 OPENCODE_CONFIG=~/.config/opencode/iop-glm.json을 주입하면 OpenCode가 두 설정을 병합한다.

API key는 평문으로 넣지 않고 반드시 환경 변수 참조를 사용한다.

{
  "provider": {
    "iop": {
      "npm": "@ai-sdk/openai-compatible",
      "options": {
        "baseURL": "http://toki-labs.com:18083/v1",
        "apiKey": "{env:IOP_OPENCODE_API_KEY}"
      }
    }
  }
}

OpenCode의 custom OpenAI-compatible provider 형식은 공식 Providers 문서를 기준으로 한다.

Ornith 35B 설정

~/.config/opencode/opencode.json의 기준값:

{
  "model": "iop/ornith:35b",
  "small_model": "iop/ornith:35b",
  "agent": {
    "ornith": {
      "mode": "primary",
      "model": "iop/ornith:35b",
      "temperature": 0.6,
      "top_p": 0.95
    }
  },
  "provider": {
    "iop": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "IOP",
      "options": {
        "baseURL": "http://toki-labs.com:18083/v1",
        "apiKey": "{env:IOP_OPENCODE_API_KEY}"
      },
      "models": {
        "ornith:35b": {
          "name": "IOP Ornith 35B",
          "family": "ornith",
          "reasoning": true,
          "temperature": true,
          "tool_call": true,
          "headers": {
            "X-IOP-Provider-Authorization": "iop-local"
          },
          "interleaved": {
            "field": "reasoning_content"
          },
          "modalities": {
            "input": ["text"],
            "output": ["text"]
          },
          "limit": {
            "context": 262144,
            "output": 32768
          }
        }
      }
    }
  }
}

temperature=0.6, top_p=0.95는 현재 dev Ornith baseline이다. X-IOP-Provider-Authorization: iop-local은 현재 dev Lemonade route에서 확인된 provider marker이며, provider 인증 정책이 바뀌면 실제 upstream credential 요구 여부를 다시 확인한다.

GLM 5.2 설정

~/.config/opencode/iop-glm.json의 기준값:

{
  "model": "iop-glm/glm-5.2",
  "small_model": "iop/ornith:35b",
  "agent": {
    "build": {
      "model": "iop-glm/glm-5.2",
      "variant": "max",
      "temperature": 1,
      "top_p": 1,
      "steps": 24
    },
    "plan": {
      "model": "iop-glm/glm-5.2",
      "variant": "max",
      "temperature": 1,
      "top_p": 1,
      "steps": 16
    }
  },
  "provider": {
    "iop-glm": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "IOP GLM",
      "options": {
        "baseURL": "http://toki-labs.com:18083/v1",
        "apiKey": "{env:IOP_OPENCODE_API_KEY}",
        "timeout": 300000,
        "chunkTimeout": 120000,
        "headers": {
          "X-IOP-Provider-Authorization": "{env:IOP_GLM_CODING_PLAN_TOKEN}"
        }
      },
      "models": {
        "glm-5.2": {
          "name": "IOP GLM-5.2",
          "family": "glm",
          "reasoning": true,
          "temperature": true,
          "tool_call": true,
          "interleaved": {
            "field": "reasoning_content"
          },
          "modalities": {
            "input": ["text"],
            "output": ["text"]
          },
          "limit": {
            "context": 1000000,
            "output": 131072
          },
          "variants": {
            "medium": {
              "reasoningEffort": "medium"
            },
            "high": {
              "reasoningEffort": "high"
            },
            "max": {
              "reasoningEffort": "max"
            }
          }
        }
      }
    }
  }
}

GLM-5.2의 1M context, 최대 131072 output, reasoning effort, sampling parameter는 Z.AI parameter 문서GLM-5.2 안내를 기준으로 한다. OpenCode의 OpenAI-compatible GLM-5.2 변형은 현재 provider transform에서 highmax를 제공하며, dispatcher의 Gemini Low fallback은 custom provider 설정에 명시한 medium variant를 사용한다.

일반 coding/agent 작업은 temperature=1, top_p=1, variant=max를 기본으로 한다. 지연이나 quota 비용을 줄여야 할 때만 agent별로 variant=high를 선택한다.

실행 wrapper

~/.local/bin/opencode는 최소한 다음 순서를 지킨다.

  1. OpenCode XDG data/state/runtime 경로를 준비한다.
  2. IOP_OPENCODE_API_KEY가 없으면 로컬 SOPS의 data를 복호화해 export한다.
  3. IOP_GLM_CODING_PLAN_TOKEN이 없으면 기존 Pi glm-5.2 model header command를 실행해 export한다.
  4. OPENCODE_CONFIG=~/.config/opencode/iop-glm.json을 export한다.
  5. 실제 OpenCode binary를 실행한다.

IOP 접속 토큰 부분의 기준 구현:

if [[ -z "${IOP_OPENCODE_API_KEY:-}" ]]; then
  IOP_OPENCODE_API_KEY="$(
    "$HOME/.local/bin/sops" -d --extract '["data"]' \
      "$HOME/.config/sops/iop/iop-opencode.sops.yaml"
  )"
fi
export IOP_OPENCODE_API_KEY

command -v opencode가 npm symlink나 임시 wrapper를 가리킬 수 있다. 최종 진입점이 위 wrapper를 건너뛰면 SOPS token과 OPENCODE_CONFIG가 적용되지 않는다.

command -v opencode
readlink "$(command -v opencode)"
sed -n '1,40p' "$(command -v opencode)"

현재 ~/.local/bin/opencode-tmp-wrapper는 다른 환경을 다시 구성하지 않고 ~/.local/bin/opencode에 그대로 위임한다.

검증

모델 등록:

opencode models iop
opencode models iop-glm

기대값:

iop/ornith:35b
iop-glm/glm-5.2

민감한 provider option을 출력하지 않고 effective model/agent 설정만 확인한다.

opencode debug config |
  jq '{
    model,
    small_model,
    build: (.agent.build | {model, variant, temperature, top_p, steps}),
    plan: (.agent.plan | {model, variant, temperature, top_p, steps}),
    ornith: (.agent.ornith | {model, temperature, top_p}),
    glm_limit: .provider["iop-glm"].models["glm-5.2"].limit,
    glm_variants: .provider["iop-glm"].models["glm-5.2"].variants,
    ornith_limit: .provider.iop.models["ornith:35b"].limit
  }'

실제 OpenCode 호출:

opencode run --agent ornith --model 'iop/ornith:35b' \
  'Reply with exactly: ORNITH_OPENCODE_OK'

opencode run --agent build --model 'iop-glm/glm-5.2' \
  'Reply with exactly: GLM52_OPENCODE_OK'

2026-08-05 검증에서는 두 명령 모두 exit code 0으로 marker를 정확히 반환했다.

장애 대응

/v1/models가 401 또는 403

  • OpenCode config의 apiKey{env:IOP_OPENCODE_API_KEY}인지 확인한다.
  • wrapper가 로컬 iop-opencode.sops.yaml을 실제로 읽는지 확인한다.
  • 원격 SOPS token을 상태 코드로 검증하고, 필요하면 평문 파일 없이 다시 재암호화한다.
  • Edge runtime config에 들어 있는 bearer 값을 사용자 토큰으로 대체 사용하지 않는다.

provider auth token required

  • IOP 접속은 성공한 상태일 수 있다.
  • GLM은 X-IOP-Provider-Authorization={env:IOP_GLM_CODING_PLAN_TOKEN}을 확인한다.
  • Ornith는 현재 dev provider marker가 model header에 들어 있는지 확인한다.
  • IOP와 upstream provider 토큰을 서로 바꾸지 않는다.

OpenCode가 다른 model/config를 사용함

  • command -v opencode와 symlink/wrapper chain을 확인한다.
  • OPENCODE_CONFIGiop-glm.json을 가리키는지 확인한다.
  • opencode debug config에서 병합된 provider와 agent를 확인한다.

SOPS가 identity did not match any recipients로 실패

  • 원격 macOS에서 SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt를 명시한다.
  • 원격 SOPS recipient와 로컬 age recipient가 다르면 파일을 그대로 복사하지 않는다.
  • 이 문서의 SSH pipe 재암호화 절차를 사용한다.

이전 auth cache가 호출에 개입함

  • wrapper가 지정한 XDG_DATA_HOME과 다른 경로의 opencode/auth.json은 정상 호출에 필요하지 않다.
  • 설정에 options.apiKey={env:IOP_OPENCODE_API_KEY}가 있으면 그 경로를 source of truth로 유지한다.
  • 사용되지 않는 캐시를 정리할 때는 provider key를 먼저 확인하고 다른 provider credential을 삭제하지 않는다.

보안 체크리스트

  • token 원문을 jq, sed, cat, set -x, shell history, 로그에 출력하지 않는다.
  • 원격 SOPS와 로컬 SOPS의 age key를 혼동하지 않는다.
  • 로컬 encrypted file 권한은 600으로 유지한다.
  • tracked opencode.json이나 문서에 실제 token을 넣지 않는다.
  • token이 노출됐으면 파일만 다시 암호화하지 말고 원격 source에서 token을 회전한다.
  • 검증 결과에는 HTTP status, token 존재 여부/길이, model id, marker만 남긴다.