TL;DR
- Apple Silicon Mac에 Codex CLI + OMX + Azure AI Foundry v1을 붙이는 과정을 정리했다.
- classic Azure OpenAI(
openai.azure.com)와 Foundry v1(cognitiveservices.azure.com) 설정은wire_api와base_url처리 방식이 다르다. - 삽질 포인트가 많아서 자동화 스크립트 3개로 정리했다.
배경
Azure AI Foundry로 프로비저닝한 리소스를 Codex CLI에 붙이려고 했다.
공식 가이드는 classic Azure OpenAI(openai.azure.com) 기준으로 작성된 게 많은데, Foundry v1 리소스는 엔드포인트 형태가 다르다.
대상 환경은 아래와 같다.
- macOS 26.x (Apple Silicon, arm64)
- Codex CLI 0.125+
- OMX v0.14+
- Azure AI Foundry 엔드포인트:
https://{resource-name}.cognitiveservices.azure.com/ - 모델:
gpt-5.4-pro(전역),gpt-5.4-mini(monitor),gpt-5.4-pro(pro)

현재 Azure for student 사용중이라 5.5는 리소스 사용이 불가하다고 떴다.
삽질 과정
1. brew install xxd — "No available formula"
brew install git curl wget unzip tmux xxd cmake pkg-config
xxd가 Homebrew에 없다는 오류가 나면서 명령 전체가 중단됐다.
xxd는 macOS에 vim과 함께 기본 내장되어 있어서 brew로 설치할 필요가 없다.
덕분에 tmux를 비롯한 나머지 패키지가 전부 설치되지 않았다.
해결은 xxd를 목록에서 빼고 재실행이다.
brew install git curl wget unzip tmux cmake pkg-config
2. jdtls Eclipse 다운로드 서버 — 50MB에 20분
공식 가이드 명령은 아래와 같다.
curl -fLo /tmp/jdtls.tar.gz \
"https://download.eclipse.org/jdtls/snapshots/jdt-language-server-latest.tar.gz"
Eclipse 서버가 40KB/s 이하로 내려받아서 예상 소요 시간이 20분 넘게 나왔다.
GitHub API로 최신 릴리즈 URL을 가져오려고 했더니 이번엔 rate limit에 걸렸다.
KeyError: 'assets'
GitHub API가 rate limit 응답을 반환하면 assets 키 자체가 없어서 파싱 오류가 난다.
결국 가장 빠른 방법은 Homebrew였다.
brew install jdtls
which jdtls # /opt/homebrew/bin/jdtls
PATH도 자동으로 잡힌다.
3. Azure /openai/deployments — 빈 배열 반환
배포된 모델을 확인하려고 deployments API를 호출했다.
curl -sH "api-key: ${AZURE_OPENAI_API_KEY}" \
"${AZURE_OPENAI_ENDPOINT%/openai/v1}/openai/deployments?api-version=2024-10-21"
JSON은 정상적으로 왔는데 data 배열이 비어있었다.
Foundry v1(cognitiveservices.azure.com) 리소스는 모델을 deployment 이름으로 배포하지 않는다.
모델 이름을 그대로 쓰는 직접 접근 방식이라 /openai/deployments 대신 /openai/models로 확인해야 한다.
curl -sH "api-key: ${AZURE_OPENAI_API_KEY}" \
"${AZURE_OPENAI_ENDPOINT%/}/openai/models?api-version=2024-10-21" \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for m in sorted(d['id'] for d in data.get('data', [])):
print(f' {m}')
"
4. 환경변수 미설정 — URL malformed
* URL rejected: Malformed input to a URL function
AZURE_OPENAI_ENDPOINT가 설정되지 않은 상태에서 curl을 실행하면 URL이 깨진다.
echo "KEY: ${AZURE_OPENAI_API_KEY:0:8}..." 출력이 KEY: ...이면 환경변수가 없는 것이다.
매 세션마다 재설정하지 않으려면 ~/.zshrc에 영구 등록이 필요하다.
export AZURE_OPENAI_ENDPOINT='https://{resource-name}.cognitiveservices.azure.com/'
export AZURE_OPENAI_API_KEY='your-api-key-here'
5. smoke test에서 python3 / nvm ✗
python3 --version이 실패했다.
Homebrew python@3.11은 python3.11 바이너리만 생성하고 python3 심볼릭 링크를 자동으로 만들지 않는다.
ln -sf /opt/homebrew/bin/python3.11 /opt/homebrew/bin/python3
nvm은 바이너리가 아니라 셸 함수다.
command -v nvm으로 체크하는 smoke test에서는 항상 ✗로 나온다.
node -v가 v22+면 실제로는 정상이다.
핵심 내용 — Foundry v1 설정
classic vs Foundry v1 차이
| 항목 | classic Azure OpenAI | Foundry v1 |
|---|---|---|
| 엔드포인트 | {name}.openai.azure.com |
{name}.cognitiveservices.azure.com |
wire_api |
"chat" |
"responses" |
| 모델 접근 | deployment name | 모델명 직접 |
| 배포 확인 | /openai/deployments |
/openai/models |
wire_api = "responses"가 Foundry v1의 핵심이다.
엔드포인트 정규화
Foundry v1 base_url은 /openai/v1 suffix가 붙어야 한다.
환경변수 값이 /로 끝나거나 /openai/v1이 없을 수 있어서 Python patch에서 자동 보정한다.
raw_ep = os.environ.get(
'AZURE_OPENAI_ENDPOINT',
'https://{resource-name}.cognitiveservices.azure.com/'
)
azure_endpoint = raw_ep.rstrip('/')
if not azure_endpoint.endswith('/openai/v1'):
azure_endpoint += '/openai/v1'
config.toml 핵심 블록 (Foundry v1)
model = "gpt-5.4-pro"
model_provider = "azure"
model_reasoning_effort = "high"
plan_mode_reasoning_effort = "xhigh"
preferred_auth_method = "apikey"
sandbox_mode = "workspace-write"
approval_policy = "on-request"
suppress_unstable_features_warning = true
[model_providers.azure]
name = "Azure AI Foundry"
base_url = "https://{resource-name}.cognitiveservices.azure.com/openai/v1"
env_key = "AZURE_OPENAI_API_KEY"
wire_api = "responses"
request_max_retries = 4
stream_max_retries = 10
stream_idle_timeout_ms = 900000
model_context_window와 model_auto_compact_token_limit은 명시하지 않는다.
issue #16068에서 custom value 설정 시 auto-compaction crash loop가 0.115 이상에서 100% 재현되고, issue #19185에서 어떤 값이든 258K로 silently reduce된다고 보고됐다.
서버 메타데이터를 신뢰하고 설정하지 않는 것이 안전하다.
자동화 스크립트 3개
설치 과정을 3개 스크립트로 분리했다.
setup_stack.sh — §4: Java 21, Python 3.11, nvm/Node 22, uv/pipx/Rust, .zshrc 통합 블록
#!/usr/bin/env zsh
set -euo pipefail
# 4.1 Java 21
brew install openjdk@21
sudo ln -sfn /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk \
/Library/Java/JavaVirtualMachines/openjdk-21.jdk
# 4.2 Python 3.11
brew install python@3.11
ln -sf /opt/homebrew/bin/python3.11 /opt/homebrew/bin/python3
# 4.3 Node 22 via nvm
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm install 22 && nvm alias default 22 && nvm use 22
# 4.4 uv + pipx + Rust
curl -LsSf https://astral.sh/uv/install.sh | sh
brew install pipx && pipx ensurepath
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
# 4.5 .zshrc CODEX_STACK 블록
grep -q '### CODEX_STACK_BEGIN ###' ~/.zshrc || cat << 'EOF' >> ~/.zshrc
### CODEX_STACK_BEGIN ###
export PATH="$HOME/.local/bin:$HOME/.local/jdtls/bin:$PATH"
export PATH="/opt/homebrew/opt/python@3.11/bin:$PATH"
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
export JAVA_HOME="/opt/homebrew/opt/openjdk@21"
export PATH="$JAVA_HOME/bin:$PATH"
export JDTLS_JVM_ARGS="-Xmx16G -Xms2G -XX:+UseG1GC -XX:+UseStringDeduplication"
### CODEX_STACK_END ###
EOF
source ~/.zshrc
setup_tools.sh — §5: Codex CLI, OMX, LSP 서버, jdtls, Spring Boot LS, LSP-MCP Bridge
#!/usr/bin/env zsh
set -euo pipefail
# 5.1 Codex CLI + OMX
node -v | grep -qE 'v(2[2-9]|[3-9][0-9])' || { echo "Node 22+ 필요"; exit 1; }
npm install -g @openai/codex oh-my-codex
# 5.2 LSP 서버
npm install -g @vtsls/language-server vscode-langservers-extracted bash-language-server
pipx install basedpyright
pipx install ruff
# 5.3 jdtls (Homebrew — Eclipse 서버보다 빠름)
brew install jdtls
# 5.4 Spring Boot Language Server (VSIX → exec JAR)
TARGET_DIR="${HOME}/.local/spring-boot-ls"
VSIX_FILE="/tmp/sbls.vsix"
mkdir -p "${TARGET_DIR}"
VSIX_URL="https://vmware.gallery.vsassets.io/_apis/public/gallery/publisher/vmware/extension/vscode-spring-boot/latest/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage"
FALLBACK_URL="https://marketplace.visualstudio.com/_apis/public/gallery/publishers/vmware/vsextensions/vscode-spring-boot/latest/vspackage"
curl -fL --connect-timeout 30 --retry 2 -H "User-Agent: Mozilla/5.0" \
-o "${VSIX_FILE}" "${VSIX_URL}" 2>/dev/null \
|| curl -fL --connect-timeout 30 --retry 2 -H "User-Agent: Mozilla/5.0" \
-o "${VSIX_FILE}" "${FALLBACK_URL}"
ZIP_MAGIC=$(xxd -l 4 "${VSIX_FILE}" | awk 'NR==1{print $2$3}')
[[ "${ZIP_MAGIC}" == "504b0304" ]] || { echo "ZIP 아님 (HTML 에러페이지 수신)"; exit 1; }
rm -rf "${TARGET_DIR:?}"/*
unzip -o -q "${VSIX_FILE}" -d "${TARGET_DIR}"
JAR_ABS=$(find "${TARGET_DIR}" -name "spring-boot-language-server-*-exec.jar" | sort -V | tail -1)
JAR_DIR=$(dirname "${JAR_ABS}")
ln -sf "$(basename "${JAR_ABS}")" "${JAR_DIR}/spring-boot-language-server.jar"
rm -f "${VSIX_FILE}"
# 5.5 LSP-MCP Bridge
mkdir -p ~/.local/src
git clone https://github.com/CesarPetrescu/lsp-mcp.git ~/.local/src/lsp-mcp
pipx install --editable ~/.local/src/lsp-mcp
setup_omx_config.sh — §8~9: OMX 초기화, config.toml patch, agents 5개, lsp-mcp Java 섹션
OMX setup 후 Python으로 config.toml을 idempotent하게 patch한다.
이미 있는 블록은 교체하고 없으면 append하는 방식이라 재실행해도 안전하다.
def replace_or_append(body, header, block):
pat = rf'\[{re.escape(header)}\].*?(?=\n\[|\Z)'
if re.search(pat, body, flags=re.DOTALL):
return re.sub(pat, block.strip() + '\n', body, flags=re.DOTALL, count=1)
return body.rstrip() + '\n\n' + block.strip() + '\n'
subagent 5개 모델 배정
| agent | model | effort | 역할 |
|---|---|---|---|
| architect | gpt-5.4-pro | xhigh | 설계 분석. read-only |
| reviewer | gpt-5.4-pro | xhigh | 코드 검증 |
| explorer | gpt-5.4-pro | low | 심볼 탐색. read-only |
| monitor | gpt-5.4-mini | low | 장기 빌드/테스트 감시 |
| pro | gpt-5.4-pro | xhigh | 수동 호출 전용 |
executor는 별도 toml 없이 전역 model = "gpt-5.4-pro"를 그대로 사용한다.
smoke test 결과
── macOS 시스템 ──
ProductVersion: 26.3.1
✓ Apple Silicon
✓ tmux 3.6a
✓ Homebrew
── 언어 런타임 ──
✓ python3
✓ java 21+
✓ JAVA_HOME=/opt/homebrew/opt/openjdk@21
── 사용자 도구 ──
✓ node
✓ uvx / cargo / pipx
✓ codex / omx
✓ jdtls
✓ basedpyright-langserver / vtsls / ruff / bash-language-server
✓ codex-lsp-bridge
── 설정 파일 ──
✓ config.toml TOML 유효
✓ hooks.json (OMX setup)
✓ agents/architect.toml ~ agents/pro.toml
✓ Azure endpoint
✓ API 키 (len=84)
✓ MCP timeout=300
── Azure 실호출 ──
✓ Azure 정상
── OMX doctor ──
Results: 15 passed, 0 warnings, 0 failed
nvm은 셸 함수라서 smoke test의 command -v nvm에서 ✗로 나온다.
node -v가 v22+이고 nvm --version이 정상이면 실제로는 문제없다.
정리
- Foundry v1 엔드포인트(
cognitiveservices.azure.com)는wire_api = "responses"+/openai/v1suffix가 필수다.wire_api = "chat"으로 두면 연결은 되도 응답 파싱이 깨진다. /openai/deployments가 빈 배열이어도 정상이다. Foundry v1은 deployment 개념 없이 모델명 직접 접근이라/openai/models로 확인해야 한다.- Homebrew 패키지 목록에 없는 formula가 하나라도 있으면
brew install전체가 중단된다.xxd처럼 macOS 내장 도구는 애초에 목록에서 빼야 한다.
'dev' 카테고리의 다른 글
| UTM에 띄운 Kali SSH 연결하기 (0) | 2026.06.24 |
|---|---|
| mac에서 vulhub의 도커 빌드가 안되는데... (0) | 2026.06.23 |
| [Infra] 단일 노드에서 고가용성 클러스터로 - 로드밸런서가 필요한 이유 (0) | 2026.05.15 |
| 클라우드 네이티브 관련 이론 정리 (5) | 2026.05.02 |
| IaC (Infrastructure as Code) 실습 (0) | 2026.04.22 |