Kilo Gateway 프로바이더 통합 설계

개요

이 문서는 기존 OpenRouter 구현을 기반으로 OpenClaw에 “Kilo Gateway”를 일급 프로바이더로 통합하기 위한 설계를 설명합니다. Kilo Gateway는 다른 기본 URL을 사용하는 OpenAI 호환 completions API를 사용합니다.

설계 결정

1. 프로바이더 명명

권장: kilocode

이유:

  • 사용자 설정 예시(프로바이더 키 kilocode)와 일치
  • 기존 프로바이더 명명 패턴과 일관성 유지 (예: openrouter, opencode, moonshot)
  • 짧고 기억하기 쉬움
  • 일반적인 “kilo” 또는 “gateway” 용어와의 혼동 방지

고려한 대안: kilo-gateway - 코드베이스에서 하이픈이 포함된 이름이 드물고 kilocode가 더 간결하여 채택하지 않음.

2. 기본 모델 참조

권장: kilocode/anthropic/claude-opus-4.6

이유:

  • 사용자 설정 예시 기반
  • Claude Opus 4.5는 뛰어난 기본 모델
  • 명시적 모델 선택으로 자동 라우팅 의존성 제거

3. 기본 URL 설정

권장: 기본값 하드코딩 + 설정 오버라이드

  • 기본 URL: https://api.kilo.ai/api/gateway/
  • 설정 가능: 예, models.providers.kilocode.baseUrl을 통해

Moonshot, Venice, Synthetic 등 다른 프로바이더와 동일한 패턴입니다.

4. 모델 스캐닝

권장: 초기에는 전용 모델 스캐닝 엔드포인트 없음

이유:

  • Kilo Gateway는 OpenRouter로 프록시하므로 모델이 동적임
  • 사용자가 설정에서 수동으로 모델 구성 가능
  • 향후 Kilo Gateway가 /models 엔드포인트를 노출하면 스캐닝 추가 가능

5. 특수 처리

권장: Anthropic 모델에 대해 OpenRouter 동작 상속

Kilo Gateway가 OpenRouter로 프록시하므로 동일한 특수 처리가 적용되어야 합니다:

  • anthropic/* 모델에 대한 캐시 TTL 적격성
  • anthropic/* 모델에 대한 추가 파라미터(cacheControlTtl)
  • OpenRouter 패턴을 따르는 트랜스크립트 정책

수정할 파일

핵심 자격 증명 관리

1. src/commands/onboard-auth.credentials.ts

추가:

export const KILOCODE_DEFAULT_MODEL_REF = "kilocode/anthropic/claude-opus-4.6";

export async function setKilocodeApiKey(key: string, agentDir?: string) {
  upsertAuthProfile({
    profileId: "kilocode:default",
    credential: {
      type: "api_key",
      provider: "kilocode",
      key,
    },
    agentDir: resolveAuthAgentDir(agentDir),
  });
}

2. src/agents/model-auth.ts

resolveEnvApiKey()envMap에 추가:

const envMap: Record<string, string> = {
  // ... 기존 항목
  kilocode: "KILOCODE_API_KEY",
};

3. src/config/io.ts

SHELL_ENV_EXPECTED_KEYS에 추가:

const SHELL_ENV_EXPECTED_KEYS = [
  // ... 기존 항목
  "KILOCODE_API_KEY",
];

설정 적용

4. src/commands/onboard-auth.config-core.ts

새 함수 추가:

export const KILOCODE_BASE_URL = "https://api.kilo.ai/api/gateway/";

export function applyKilocodeProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
  const models = { ...cfg.agents?.defaults?.models };
  models[KILOCODE_DEFAULT_MODEL_REF] = {
    ...models[KILOCODE_DEFAULT_MODEL_REF],
    alias: models[KILOCODE_DEFAULT_MODEL_REF]?.alias ?? "Kilo Gateway",
  };

  const providers = { ...cfg.models?.providers };
  const existingProvider = providers.kilocode;
  const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
    string,
    unknown
  > as { apiKey?: string };
  const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
  const normalizedApiKey = resolvedApiKey?.trim();

  providers.kilocode = {
    ...existingProviderRest,
    baseUrl: KILOCODE_BASE_URL,
    api: "openai-completions",
    ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
  };

  return {
    ...cfg,
    agents: {
      ...cfg.agents,
      defaults: {
        ...cfg.agents?.defaults,
        models,
      },
    },
    models: {
      mode: cfg.models?.mode ?? "merge",
      providers,
    },
  };
}

export function applyKilocodeConfig(cfg: OpenClawConfig): OpenClawConfig {
  const next = applyKilocodeProviderConfig(cfg);
  const existingModel = next.agents?.defaults?.model;
  return {
    ...next,
    agents: {
      ...next.agents,
      defaults: {
        ...next.agents?.defaults,
        model: {
          ...(existingModel && "fallbacks" in (existingModel as Record<string, unknown>)
            ? {
                fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
              }
            : undefined),
          primary: KILOCODE_DEFAULT_MODEL_REF,
        },
      },
    },
  };
}

인증 선택 시스템

5. src/commands/onboard-types.ts

AuthChoice 타입에 추가:

export type AuthChoice =
  // ... 기존 선택지
  "kilocode-api-key";
// ...

OnboardOptions에 추가:

export type OnboardOptions = {
  // ... 기존 옵션
  kilocodeApiKey?: string;
  // ...
};

6. src/commands/auth-choice-options.ts

AuthChoiceGroupId에 추가:

export type AuthChoiceGroupId =
  // ... 기존 그룹
  "kilocode";
// ...

AUTH_CHOICE_GROUP_DEFS에 추가:

{
  value: "kilocode",
  label: "Kilo Gateway",
  hint: "API key (OpenRouter-compatible)",
  choices: ["kilocode-api-key"],
},

buildAuthChoiceOptions()에 추가:

options.push({
  value: "kilocode-api-key",
  label: "Kilo Gateway API key",
  hint: "OpenRouter-compatible gateway",
});

7. src/commands/auth-choice.preferred-provider.ts

매핑 추가:

const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
  // ... 기존 매핑
  "kilocode-api-key": "kilocode",
};

인증 선택 적용

8. src/commands/auth-choice.apply.api-providers.ts

임포트 추가:

import {
  // ... 기존 임포트
  applyKilocodeConfig,
  applyKilocodeProviderConfig,
  KILOCODE_DEFAULT_MODEL_REF,
  setKilocodeApiKey,
} from "./onboard-auth.js";

kilocode-api-key 처리 추가:

if (authChoice === "kilocode-api-key") {
  const store = ensureAuthProfileStore(params.agentDir, {
    allowKeychainPrompt: false,
  });
  const profileOrder = resolveAuthProfileOrder({
    cfg: nextConfig,
    store,
    provider: "kilocode",
  });
  const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId]));
  const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined;
  let profileId = "kilocode:default";
  let mode: "api_key" | "oauth" | "token" = "api_key";
  let hasCredential = false;

  if (existingProfileId && existingCred?.type) {
    profileId = existingProfileId;
    mode =
      existingCred.type === "oauth" ? "oauth" : existingCred.type === "token" ? "token" : "api_key";
    hasCredential = true;
  }

  if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "kilocode") {
    await setKilocodeApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
    hasCredential = true;
  }

  if (!hasCredential) {
    const envKey = resolveEnvApiKey("kilocode");
    if (envKey) {
      const useExisting = await params.prompter.confirm({
        message: `Use existing KILOCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
        initialValue: true,
      });
      if (useExisting) {
        await setKilocodeApiKey(envKey.apiKey, params.agentDir);
        hasCredential = true;
      }
    }
  }

  if (!hasCredential) {
    const key = await params.prompter.text({
      message: "Enter Kilo Gateway API key",
      validate: validateApiKeyInput,
    });
    await setKilocodeApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
    hasCredential = true;
  }

  if (hasCredential) {
    nextConfig = applyAuthProfileConfig(nextConfig, {
      profileId,
      provider: "kilocode",
      mode,
    });
  }
  {
    const applied = await applyDefaultModelChoice({
      config: nextConfig,
      setDefaultModel: params.setDefaultModel,
      defaultModel: KILOCODE_DEFAULT_MODEL_REF,
      applyDefaultConfig: applyKilocodeConfig,
      applyProviderConfig: applyKilocodeProviderConfig,
      noteDefault: KILOCODE_DEFAULT_MODEL_REF,
      noteAgentModel,
      prompter: params.prompter,
    });
    nextConfig = applied.config;
    agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
  }
  return { config: nextConfig, agentModelOverride };
}

함수 상단에 tokenProvider 매핑도 추가:

if (params.opts.tokenProvider === "kilocode") {
  authChoice = "kilocode-api-key";
}

CLI 등록

9. src/cli/program/register.onboard.ts

CLI 옵션 추가:

.option("--kilocode-api-key <key>", "Kilo Gateway API key")

액션 핸들러에 추가:

kilocodeApiKey: opts.kilocodeApiKey as string | undefined,

auth-choice 도움말 텍스트 업데이트:

.option(
  "--auth-choice <choice>",
  "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|kilocode-api-key|ai-gateway-api-key|...",
)

비대화형 온보딩

10. src/commands/onboard-non-interactive/local/auth-choice.ts

kilocode-api-key 처리 추가:

if (authChoice === "kilocode-api-key") {
  const resolved = await resolveNonInteractiveApiKey({
    provider: "kilocode",
    cfg: baseConfig,
    flagValue: opts.kilocodeApiKey,
    flagName: "--kilocode-api-key",
    envVar: "KILOCODE_API_KEY",
  });
  await setKilocodeApiKey(resolved.apiKey, agentDir);
  nextConfig = applyAuthProfileConfig(nextConfig, {
    profileId: "kilocode:default",
    provider: "kilocode",
    mode: "api_key",
  });
  // ... 기본 모델 적용
}

내보내기 업데이트

11. src/commands/onboard-auth.ts

내보내기 추가:

export {
  // ... 기존 내보내기
  applyKilocodeConfig,
  applyKilocodeProviderConfig,
  KILOCODE_BASE_URL,
} from "./onboard-auth.config-core.js";

export {
  // ... 기존 내보내기
  KILOCODE_DEFAULT_MODEL_REF,
  setKilocodeApiKey,
} from "./onboard-auth.credentials.js";

특수 처리 (선택 사항)

12. src/agents/pi-embedded-runner/cache-ttl.ts

Anthropic 모델에 대한 Kilo Gateway 지원 추가:

export function isCacheTtlEligibleProvider(provider: string, modelId: string): boolean {
  const normalizedProvider = provider.toLowerCase();
  const normalizedModelId = modelId.toLowerCase();
  if (normalizedProvider === "anthropic") return true;
  if (normalizedProvider === "openrouter" && normalizedModelId.startsWith("anthropic/"))
    return true;
  if (normalizedProvider === "kilocode" && normalizedModelId.startsWith("anthropic/")) return true;
  return false;
}

13. src/agents/transcript-policy.ts

Kilo Gateway 처리 추가 (OpenRouter와 유사):

const isKilocodeGemini = provider === "kilocode" && modelId.toLowerCase().includes("gemini");

// needsNonImageSanitize 검사에 포함
const needsNonImageSanitize =
  isGoogle || isAnthropic || isMistral || isOpenRouterGemini || isKilocodeGemini;

설정 구조

사용자 설정 예시

{
  "models": {
    "mode": "merge",
    "providers": {
      "kilocode": {
        "baseUrl": "https://api.kilo.ai/api/gateway/",
        "apiKey": "xxxxx",
        "api": "openai-completions",
        "models": [
          {
            "id": "anthropic/claude-opus-4.6",
            "name": "Anthropic: Claude Opus 4.6"
          },
          { "id": "minimax/minimax-m2.5:free", "name": "Minimax: Minimax M2.5" }
        ]
      }
    }
  }
}

인증 프로필 구조

{
  "profiles": {
    "kilocode:default": {
      "type": "api_key",
      "provider": "kilocode",
      "key": "xxxxx"
    }
  }
}

테스트 고려사항

  1. 단위 테스트:

    • setKilocodeApiKey()가 올바른 프로필을 기록하는지 테스트
    • applyKilocodeConfig()가 올바른 기본값을 설정하는지 테스트
    • resolveEnvApiKey("kilocode")가 올바른 환경 변수를 반환하는지 테스트
  2. 통합 테스트:

    • --auth-choice kilocode-api-key로 온보딩 흐름 테스트
    • --kilocode-api-key로 비대화형 온보딩 테스트
    • kilocode/ 접두사로 모델 선택 테스트
  3. E2E 테스트:

    • Kilo Gateway를 통한 실제 API 호출 테스트 (라이브 테스트)

마이그레이션 참고사항

  • 기존 사용자에 대한 마이그레이션 불필요
  • 신규 사용자는 kilocode-api-key 인증 선택을 즉시 사용 가능
  • kilocode 프로바이더를 수동 설정한 기존 구성은 계속 동작

향후 고려사항

  1. 모델 카탈로그: Kilo Gateway가 /models 엔드포인트를 노출하면, scanOpenRouterModels()와 유사한 스캐닝 지원 추가

  2. OAuth 지원: Kilo Gateway가 OAuth를 추가하면 인증 시스템 확장

  3. 속도 제한: 필요 시 Kilo Gateway 전용 속도 제한 처리 추가

  4. 문서화: 설정 및 사용법을 설명하는 docs/providers/kilocode.md 문서 추가

변경사항 요약

파일변경 유형설명
src/commands/onboard-auth.credentials.ts추가KILOCODE_DEFAULT_MODEL_REF, setKilocodeApiKey()
src/agents/model-auth.ts수정envMapkilocode 추가
src/config/io.ts수정셸 환경 키에 KILOCODE_API_KEY 추가
src/commands/onboard-auth.config-core.ts추가applyKilocodeProviderConfig(), applyKilocodeConfig()
src/commands/onboard-types.ts수정AuthChoicekilocode-api-key, 옵션에 kilocodeApiKey 추가
src/commands/auth-choice-options.ts수정kilocode 그룹 및 옵션 추가
src/commands/auth-choice.preferred-provider.ts수정kilocode-api-key 매핑 추가
src/commands/auth-choice.apply.api-providers.ts수정kilocode-api-key 처리 추가
src/cli/program/register.onboard.ts수정--kilocode-api-key 옵션 추가
src/commands/onboard-non-interactive/local/auth-choice.ts수정비대화형 처리 추가
src/commands/onboard-auth.ts수정새 함수 내보내기
src/agents/pi-embedded-runner/cache-ttl.ts수정kilocode 지원 추가
src/agents/transcript-policy.ts수정kilocode Gemini 처리 추가