Skip to content

feat(agent-core-v2): accept HEIC, HEIF, and BMP images for Kimi models - #3649

Merged
RealKai42 merged 4 commits into
mainfrom
feat/provider-aware-image-formats
Sep 8, 2026
Merged

feat(agent-core-v2): accept HEIC, HEIF, and BMP images for Kimi models#3649
RealKai42 merged 4 commits into
mainfrom
feat/provider-aware-image-formats

Conversation

@RealKai42

@RealKai42 RealKai42 commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

ReadMediaFile refused iPhone photos (.heic) with "which the provider does not accept. Convert it to JPEG first", even though the Kimi API accepts HEIC/HEIF/BMP natively (formats are detected server-side from magic bytes, and the upload pipeline re-encodes everything to WebP anyway). The refusal came from a single global image-format allowlist (PNG/JPEG/GIF/WebP) that was shared by every ingestion point and did not know which provider the agent was talking to.

Two further blockers sat behind that gate:

  • ReadMediaFile cannot re-encode HEIC locally, and its default path capped the delivered image at the 256KB read budget, so a typical 1–4MB iPhone photo would have failed with "too large to send safely" even once the format was allowed.
  • The Anthropic protocol lowering had its own hard-coded PNG/JPEG/GIF/WebP allowlist, so Kimi reached over the Anthropic-compatible protocol would still have rejected HEIC client-side.

What changed

Provider-keyed image policy. A small vocabulary module in the human layer (llm/media/image-formats) defines a ProviderImagePolicy per provider type: Kimi accepts PNG/JPEG/GIF/WebP/BMP/HEIC/HEIF with a 5MB per-image inline budget; every other provider keeps the baseline four formats and the existing 3.75MB budget. The agent-side policy helpers (isModelAcceptedImageMime, gateImageFormatParts, buildUnsupportedImageNotice, unsupportedImageMimeFromUrl, compressImageContentParts) take an optional providerType and consult it, and the refusal notice now names the current provider's accepted formats.

Provider type threaded to every gate. IAgentProfileService.getModelProviderType() exposes the bound model's provider type; it is passed into ReadMediaFile by the media tools registrar (same pattern as inlineVideoSupported), into the prompt-entry gate in the prompt service, into MCP tool results (looked up at call time so a later model switch is honored), into session media references (from the requester's model), and into the kap-server prompt and skill routes via a new providerType option on resolvePromptMediaFiles.

Inline passthrough for formats we cannot re-encode. In ReadMediaFile, an accepted format that the local compressor cannot decode (HEIC/HEIF/BMP/GIF/animated WebP) is now sent untouched up to max(read byte budget, provider inline budget) instead of the 256KB read budget; above that the tool returns a conversion command (sips/heif-convert/magick) since converting to JPEG re-enables local downsampling. The full_resolution cap uses the same provider budget, so it stays at 3.75MB for non-Kimi providers and becomes 5MB for Kimi. Rationale for 5MB: 3.75MB was derived from Anthropic's 5MB limit minus base64 overhead and has no basis for Kimi; 5MB covers 12MP and 24MP iPhone HEICs while keeping the per-turn resend cost bounded.

Anthropic lowering follows the bound provider. The base64 image allowlist in the Anthropic lowering now comes from the same policy keyed by ctx.model.provider, so Kimi over the Anthropic-compatible protocol forwards HEIC while real Anthropic still fails fast.

acp-server leaves format gating to the engine. The ACP edge does not know the bound provider, and the engine's prompt pipeline already gates every prompt image (with the same notices and alias canonicalization), so the redundant edge gate was removed and the stale comment claiming the engine never gates was corrected. Compression still happens at the edge.

The runtime safety net is unchanged: if a provider still rejects an image format, the error is classified as image_format and the request is resent with media stripped.

Tests. Red-first for each slice: policy helpers (image-compress.test.ts), ReadMediaFile with and without a Kimi provider including the 5MB boundary, the registrar handing the provider type through, getModelProviderType, prompt-entry gating, MCP resource links and inline images (including call-time lookup), session media references, kap-server inline HEIC on a Kimi-typed provider, the Anthropic lowering, and the klient e2e matrix (an unaccepted-everywhere format is still replaced on every provider; a Kimi-only format reaches Kimi and is replaced elsewhere). Existing tests that used BMP as the "unaccepted" sample were switched to TIFF.

Follow-up after review

  • Anthropic lowering read the wrong provider id. LlmModel.provider is the route id (anthropic) on the production path, not the model's provider type, so the lowering still refused HEIC for Kimi over the Anthropic-compatible protocol. The accepted set now comes from a ProtocolTrait.acceptedImageMimes hook implemented by the Kimi traits; the test goes through the real kimiAnthropicTrait with the routed provider id, including a requester-level check of the wire body.
  • kap-server gated media against the previously bound model. When the same prompt request selects another model via model, media preparation now resolves that alias's provider type (getModelProviderType(alias)); a request that only switches profile still uses the bound model's provider.
  • Session media memo re-judged per provider. A memoized inline image is no longer returned to a provider that does not accept its format; the uncached path degrades it to the path tag as before.
  • ReadMediaFile is rebuilt when the alias's provider changes. The registrar key now includes the model's provider type and protocol, so a config reload that re-points an alias refreshes the tool (this also closes the pre-existing gap for the inline-video decision, which depends on protocol).

Not changed on purpose: re-gating historical images after a provider switch (the existing image_format recovery already strips media and resends, so the session stays usable; a provider-aware history projection is a separate change), and enforcing the 5MB inline budget at prompt/MCP ingress (that budget is ReadMediaFile's decision boundary where the model can convert the file; user-supplied attachments keep the existing 64MB/10MB caps, as GIF always has).

Key the accepted image MIME set by the provider the agent is bound to
instead of a single global allowlist: Kimi accepts PNG/JPEG/GIF/WebP plus
BMP/HEIC/HEIF, every other provider keeps the baseline set. The provider
image policy lives in the human layer (`llm/media/image-formats`) so both
the agent-side gates and the Anthropic protocol lowering share it.

- thread the provider type through every ingestion gate: ReadMediaFile,
  prompt entry, MCP tool results, session media references, kap-server
  prompt/skill routes; expose it via IAgentProfileService.getModelProviderType
- ReadMediaFile sends accepted formats it cannot re-encode locally inline
  up to the provider's per-image budget (5MB for Kimi, 3.75MB otherwise)
  and points at a conversion command above that; full_resolution uses the
  same provider budget
- acp-server no longer gates formats itself and leaves the decision to the
  engine prompt pipeline, which knows the bound provider
- the Anthropic lowering allowlist follows the bound provider so Kimi over
  the Anthropic-compatible protocol no longer rejects HEIC client-side
# Conflicts:
#	packages/agent-core-v2/src/agent/prompt/promptService.ts
@changeset-bot

changeset-bot Bot commented Sep 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: afbc8e9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-08T11:20:54.050280Z afbc8e9 New commits
🔒 Security Review Completed 2026-09-08T11:19:03.989276Z afbc8e9 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 8, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@afbc8e9
npx https://pkg.pr.new/@moonshot-ai/kimi-code@afbc8e9

commit: afbc8e9

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04c878377e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/agent-core-v2/src/human/llm/requester/bases/anthropic/lower.ts Outdated
Comment thread packages/kap-server/src/routes/prompts.ts Outdated
Comment thread packages/agent-core-v2/src/agent/prompt/promptService.ts
Comment thread packages/agent-core-v2/src/agent/media/mediaResolverService.ts Outdated
Comment thread packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts
Comment thread packages/agent-core-v2/src/human/llm/media/image-formats.ts
… paths

Follow-ups from review of the provider-aware image formats:

- the Anthropic lowering read the policy from `LlmModel.provider`, which
  the adapter registry sets to the route id (`anthropic`) rather than the
  model's provider type, so Kimi over the Anthropic-compatible protocol
  still rejected HEIC client-side; the accepted set now comes from a
  `ProtocolTrait.acceptedImageMimes` hook that the Kimi traits implement,
  and the test goes through the real trait with the routed provider id
- kap-server prompt ingestion gated media against the currently bound
  model even when the same request selected another model; the profile
  service now resolves the provider type for a requested alias
- the session media memo served a cached inline image to a provider that
  does not accept its format; memo hits are re-judged against the current
  provider before being returned
- the media tools registrar keyed ReadMediaFile only by alias and
  capabilities, so a reload that changed the alias's provider type or
  protocol kept the stale tool; both now take part in the key

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: afbc8e9865

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +444 to +446
if (inlineOnly) {
const inlineLimit = Math.max(readByteBudget, this.inlineImageByteBudget);
if (compressed.finalByteLength > inlineLimit) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the edge limit for non-recodable images

When an accepted non-recodable image is within the inline byte limit but exceeds maxEdge—for example, a wide GIF, BMP, or animated WebP—the inlineOnly branch checks only its byte length and skips the dimension check on lines 458–460. ReadMediaFile therefore sends it unchanged even in the default mode, whereas the previous path rejected images exceeding the configured/default edge ceiling and reserved native delivery for full_resolution; keep enforcing maxEdge here and return conversion guidance when the format cannot be resized.

Useful? React with 👍 / 👎.

@RealKai42
RealKai42 merged commit 80480c0 into main Sep 8, 2026
15 checks passed
@RealKai42
RealKai42 deleted the feat/provider-aware-image-formats branch September 8, 2026 11:28
@github-actions github-actions Bot mentioned this pull request Sep 8, 2026
arrrrny added a commit to arrrrny/kimi-code-sync that referenced this pull request Sep 9, 2026
Sync 147 upstream commits onto the fork. Notable upstream changes:
- Remove the protocol package and relocate wire types into
  agent-core-v2/src/events.ts (MoonshotAI#3646)
- Absorb stepRetry into the LLM requester turn state machine (already
  followed in the 2026-09-07 sync)
- NotifyUser tool + mid-turn update panel (MoonshotAI#3524)
- HEIC/HEIF/BMP image support (MoonshotAI#3649), resumable large file reads
  (MoonshotAI#3645), reasoning_details round-trip (MoonshotAI#3492), zstd updater
  manifests (MoonshotAI#3669)

Conflict resolutions (all by hand, fork features preserved):
- package.json: drop the empty simple-git-hooks block (upstream cleanup;
  not a fork feature).
- docs/config-manifest.toml: take the union, then regenerate with
  pnpm gen:config-manifest (33 sections, fork sections intact).
- test/agent/stepRetry/stepRetry.test.ts: keep upstream's slimmed
  retryBackoffDelays test — the fork had deleted the old fat version
  when stepRetry was absorbed; the util is fork-used code with no other
  coverage.
- protocol/src/events.ts: accept upstream's package removal; the fork's
  compaction.started model/model_display fields already auto-merged
  into their new home (agent/fullCompaction/compactionOps.ts). No source
  still imports @moonshot-ai/protocol.

Merge follow-ups:
- Regenerate state/wire/config manifests; freshness tests pass.
- node-sdk update-all-session-models flag test: drop the v1-engine
  parity half (legacy agent-core v1 is removed upstream); keep the v2
  registration pin.
- Fix pre-existing lint errors in merge-touched files so the
  lint-staged pre-commit gate passes: unused imports/variables
  (sdk-rpc-client-v2.ts, tui/commands/config.ts,
  check-import-boundaries.mjs), floating promises (sdk-rpc-client-v2.ts,
  fullCompaction.test.ts).
- Add compactionOps.ts (model_display) to .github/FORK_OWNED_FILES so
  the relocated compaction-model fields are guard-listed.

Verification:
- pnpm run typecheck green; pnpm run lint at fork baseline (no new
  findings vs pre-merge HEAD).
- FORK_OWNED_FILES markers all resolve.
- Fork suites pass: TUI commands + session CLI (75 tests),
  x-opencode-session (7), update-all-session-models flag, oauth
  openai-compatible.
- fullCompaction.test.ts: 2 failures, both pre-existing at pre-merge
  HEAD (baseline had 5; upstream's macOS stabilization fixed 3.
EOF
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant