Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 47 additions & 19 deletions request-logger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ If you are not sure, pick "not sure". The tool still logs everything; it just
shows you the raw JSON instead of a fully readable render, because it cannot
safely guess a shape you did not tell it. See "What you get" below.

For **OpenCode** with an **OpenAI-compatible** custom target, and for **every**
**Pi** custom target, the wizard also tries the unauthenticated `/v1/models`
endpoint and offers any model IDs it finds. You can always enter an ID
manually, and the wizard falls back to manual entry if discovery fails.
Endpoints that require credentials for model listing are not supported.
For **OpenCode** with an **OpenAI-compatible** custom target, for **every**
**Pi** custom target, and for **every Junie** target, the wizard also tries
the unauthenticated `/v1/models` endpoint and offers any model IDs it finds.
You can always enter an ID manually, and the wizard falls back to manual
entry if discovery fails. Endpoints that require credentials for model
listing are not supported.

- OpenCode's printed command uses a temporary `OPENCODE_CONFIG_CONTENT`
provider for that one run, merged with your existing OpenCode configuration
Expand All @@ -62,6 +63,10 @@ Endpoints that require credentials for model listing are not supported.
endpoint, which an Anthropic-compatible server often does not expose. If
discovery finds nothing there, that is expected, and typing the model ID by
hand is the normal path, not a sign something is broken.
- Junie asks for a model on every wire format too, for a different reason
than Pi: Junie sends the model profile's `id` verbatim as the `model` field
of every request, so it has to be a real model your backend recognizes, not
a placeholder — see the Junie section below.

To change a remembered answer:

Expand Down Expand Up @@ -139,16 +144,33 @@ is not wired up yet — ask for it via the issue tracker if you hit this.
### Junie

Junie is BYOK across several backends, with no fixed host of its own — the
same shape as OMP — so it goes through the base URL and wire format
same shape as OMP — so it goes through the base URL, wire format and model
questions like a custom target does, picking a real Anthropic-compatible or
OpenAI-compatible template depending which you choose. It writes a proxy
entry to `~/.junie/config.json` (user scope; a project-scope file at
`<project-root>/.junie/config.json` takes precedence if you have one — see
Junie's own docs), merged in alongside whatever else is already there.
OpenAI-compatible template depending which you choose. It writes a **custom
model profile** to `~/.junie/models/request-logger.json`, and the command it
prints selects that profile explicitly with `--model custom:request-logger`
— Junie only uses a custom model when asked for by name.

The model you pick goes into the profile's `id` field, which Junie sends
verbatim as the `model` field of every request — it is not a label for this
tool to key off of. An earlier version of this profile hard-coded `id` to
`"request-logger"`, which is not a real model on any backend: Anthropic
rejected every request with a 404 (`model: request-logger`), confirmed
against a live proxy. Pick a model your backend actually serves (e.g.
`claude-sonnet-4-5` against the real Anthropic API).

Junie's own docs also describe a different mechanism for this: a `proxies`
entry in `~/.junie/config.json` with a `kind` of `"Anthropic"` or `"OpenAI"`.
Do not use that instead. It was tried first, and driven against a real Junie
CLI install (build 1543.24) it does not work — Junie never consults it,
headless or interactive, no matter what `kind` is set to, and just keeps
talking to its normal authenticated backend with no error and an empty logs
folder. The custom-model route above is the one that was actually driven
end to end, with a real proxy in front of it, on both wire formats.

Junie is the one agent here with no existing login for this tool to pass
through. Its custom-proxy mechanism bypasses JetBrains AI authentication
entirely, so the printed config carries a placeholder header line and you
through. Its custom-model mechanism bypasses JetBrains AI authentication
entirely, so the printed profile carries a placeholder header line and you
paste in a real API key yourself — an Anthropic key on the
Anthropic-compatible route, an OpenAI key on the OpenAI-compatible one.
Treat that file the way you would any other file holding a real key: do not
Expand Down Expand Up @@ -315,13 +337,19 @@ Be fair to the tool when you judge a failure.
full course of the lesson. Claude Code on Google Vertex AI is in this
group — verified against Anthropic's own Vertex documentation, not yet
driven against a real Vertex project.
- **Junie is the least-verified entry in the catalogue.** Junie CLI is
closed source, so unlike every other agent here its entry was not checked
against real source, only against JetBrains' published Junie CLI docs
(`config.json`, custom proxies, and CLI reference). It has not been driven
against a real Junie install. If the proxy `kind`, the config file's
precise shape, or the header format is wrong, that is the likely reason,
and the fix is one line in `agents.ts`.
- **Junie was driven against a real install (build 1543.24)**, unlike every
other closed-source claim in this list. That test is exactly why this
entry no longer follows Junie's own documented `proxies`/`config.json`
mechanism: on that real install, it never took effect, regardless of
`kind`, with no error to say so. The custom-model route it uses instead
(`~/.junie/models/*.json` selected via `--model custom:<name>`) was
confirmed to actually deliver requests to a local listener, on both wire
formats, with the correct endpoint path in each case. What is not verified
is everything upstream of that: Junie's task-completion behavior, its
interactive TUI, and whether a different install or a future release
changes any of this. If something here breaks, that is the likely reason,
and the fix is likely still one line in `agents.ts` — just possibly a
different line than the `kind` field this note used to point at.
- **A custom base URL is only as tested as the agent it is attached to.** The
base URL and wire format mechanism itself is tested directly (see
`agents.test.ts`); a specific third-party server behind it has not
Expand Down
107 changes: 90 additions & 17 deletions request-logger/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1432,52 +1432,108 @@ describe("resolveChoice — OMP", () => {
// ---------------------------------------------------------------------------

describe("resolveChoice — Junie", () => {
it("resolves Junie straight from a base URL and wire format, with no provider needed", () => {
it("resolves Junie straight from a base URL, wire format and model, with no provider needed", () => {
const result = resolveChoice(
{
agent: "junie",
customBaseUrl: "http://localhost:8787",
customRenderer: "anthropic",
customModel: "claude-sonnet-4-5",
},
PORT
);
expect(result.kind).toBe("custom-target");
});

it("gives Junie its own bin, with no env vars", () => {
it("tells Junie which custom model to use, with the endpoint path folded into the base URL", () => {
const result = customTarget({
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "anthropic",
customModel: "claude-sonnet-4-5",
});
expect(result.command).toBe("junie");
expect(result.command).toBe("junie --model custom:request-logger");
expect(result.baseUrl).toBe("http://localhost:8787/v1/messages");
});

it("writes an Anthropic-kind proxy entry for the anthropic-compatible route", () => {
it("uses the OpenAI chat/completions path for the openai-compatible route", () => {
const result = customTarget({
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "openai",
customModel: "gpt-4.1",
});
expect(result.baseUrl).toBe("http://localhost:8787/v1/chat/completions");
});

it("writes an Anthropic model profile for the anthropic-compatible route", () => {
const result = customTarget({
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "anthropic",
customModel: "claude-sonnet-4-5",
});
expect(result.setup).toHaveLength(1);
expect(result.setup[0].path).toBe("~/.junie/config.json");
expect(result.setup[0].path).toBe("~/.junie/models/request-logger.json");
expect(result.setup[0].language).toBe("json");
expect(result.setup[0].body).toContain('"kind": "Anthropic"');
expect(result.setup[0].body).toContain('"apiType": "Anthropic"');
expect(result.setup[0].body).toContain(
'"api-url": "http://localhost:8787"'
'"baseUrl": "http://localhost:8787/v1/messages"'
);
expect(result.setup[0].body).toContain("x-api-key:");
expect(result.setup[0].body).toContain('"provider": "request-logger"');
expect(result.setup[0].body).toContain('"x-api-key":');
});

it("writes an OpenAI-kind proxy entry for the openai-compatible route", () => {
it("writes the chosen model into the profile's id field, since Junie sends it verbatim as the model on every request", () => {
// An earlier version hard-coded "id" to "request-logger", which is not a
// real model on any backend -- Anthropic rejected it with a 404 ("model:
// request-logger"), confirmed against a live proxy.
const result = customTarget({
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "anthropic",
customModel: "claude-sonnet-4-5",
});
expect(result.setup[0].body).toContain('"id": "claude-sonnet-4-5"');
expect(result.setup[0].body).not.toContain("request-logger");
});

it("rejects a Junie custom target with no model chosen", () => {
const result = resolveChoice(
{
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "anthropic",
},
PORT
);
expect(result.kind).toBe("error");
});

it("does not set anthropic-version itself, since Junie already sends one", () => {
// Junie's Anthropic client always sends its own anthropic-version
// header. An extraHeaders entry with the same name does not replace
// that, it is appended as a second occurrence, and the two get joined
// with a comma on the wire (e.g. "2023-06-01,2023-06-01") -- which
// Anthropic rejects as an invalid version code. Confirmed against a
// live proxy: this is what was producing a 400 on every request.
const result = customTarget({
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "anthropic",
customModel: "claude-sonnet-4-5",
});
expect(result.setup[0].body).not.toContain("anthropic-version");
});

it("writes an OpenAICompletion model profile for the openai-compatible route", () => {
const result = customTarget({
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "openai",
customModel: "gpt-4.1",
});
expect(result.setup[0].body).toContain('"kind": "OpenAI"');
expect(result.setup[0].body).toContain("Authorization: Bearer");
expect(result.setup[0].body).toContain('"apiType": "OpenAICompletion"');
expect(result.setup[0].body).toContain('"Authorization": "Bearer');
});

it("defaults Junie's raw/not-sure custom target to the OpenAI-kind template", () => {
Expand All @@ -1488,43 +1544,60 @@ describe("resolveChoice — Junie", () => {
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "raw",
customModel: "some-model",
});
expect(result.setup[0].body).toContain('"kind": "OpenAI"');
expect(result.setup[0].body).toContain('"apiType": "OpenAICompletion"');
});

it("never sends Junie's Anthropic-kind api-key header on the OpenAI route, or vice versa", () => {
const anthropicRoute = customTarget({
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "anthropic",
customModel: "claude-sonnet-4-5",
});
const openaiRoute = customTarget({
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "openai",
customModel: "gpt-4.1",
});
expect(anthropicRoute.setup[0].body).not.toContain("Authorization: Bearer");
expect(openaiRoute.setup[0].body).not.toContain("x-api-key:");
expect(anthropicRoute.setup[0].body).not.toContain('"Authorization":');
expect(openaiRoute.setup[0].body).not.toContain('"x-api-key":');
});

it("warns that Junie's proxy bypasses JetBrains AI authentication, unlike every login-based agent here", () => {
it("warns that Junie's model profile bypasses JetBrains AI authentication, unlike every login-based agent here", () => {
const result = customTarget({
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "anthropic",
customModel: "claude-sonnet-4-5",
});
expect(result.notes.some((note) => note.includes("bypasses"))).toBe(true);
});

it("warns that Junie's documented config.json proxy route does not actually work", () => {
const result = customTarget({
agent: "junie",
customBaseUrl: "http://localhost:11434",
customRenderer: "anthropic",
customModel: "claude-sonnet-4-5",
});
expect(
result.notes.some((note) => note.includes("never consulted"))
).toBe(true);
});

it("still resolves Junie even when the saved provider field is stale", () => {
// alwaysCustom agents ignore whatever is saved under `provider`; only
// the custom base URL and renderer matter.
// the custom base URL, renderer and model matter.
const result = resolveChoice(
{
agent: "junie",
provider: "whatever-was-saved-before",
customBaseUrl: "http://localhost:11434",
customRenderer: "raw",
customModel: "some-model",
},
PORT
);
Expand Down
Loading