This proposal adds native energy and carbon tracking to OpenCode by:
- Capturing measured energy data from providers that supply it (GreenPT, Neuralwatt)
- Estimating energy for providers that don't measure it
- Storing energy data alongside existing token/cost data
- Providing plugin hooks for customization and external integration
AI inference has significant energy and carbon implications. Tools like llm-greenpt and llm-neuralwatt have shown that this data can be captured and made visible to users. OpenCode should make this a first-class feature.
- Non-breaking - All changes are additive; existing code continues to work
- Provider-agnostic - Works with any provider, measured or estimated
- Plugin-extensible - Plugins can enhance, modify, or replace energy calculations
- Visible - Energy data appears in UI alongside cost
┌─────────────────────────────────────────────────────────────────────────────┐
│ Provider API │
│ │
│ Standard providers: Energy-aware providers: │
│ - OpenAI - GreenPT (impact in response) │
│ - Anthropic - Neuralwatt (energy in SSE comment) │
│ - Google - Future providers... │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ SDK Layer (Modified) │
│ │
│ openai-compatible/openai-responses-language-model.ts │
│ │
│ Changes: │
│ • Extended chunk schema to capture `impact` field │
│ • Custom SSE handler to capture `: energy` comments │
│ • Energy data passed via providerMetadata.energy │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ providerMetadata.energy
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Session Processor (Modified) │
│ │
│ processor.ts │
│ │
│ Changes: │
│ • Check for measured energy in providerMetadata │
│ • Call plugin hook for custom estimates if not measured │
│ • Fall back to built-in estimation │
│ • Call plugin hook to allow enhancement │
│ • Store energy in StepFinishPart and AssistantMessage │
│ • Publish energy event │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Storage │ │ Plugin Hooks │ │ UI │
│ │ │ │ │ │
│ message.json │ │ energy.estimate│ │ Context usage │
│ + energy {} │ │ energy │ │ + energy │
│ │ │ │ │ │
│ part.json │ │ (custom calc, │ │ Tooltips │
│ + energy {} │ │ logging, │ │ + energy │
│ │ │ external svc)│ │ │
└───────────────┘ └───────────────┘ └───────────────┘
1. Provider sends energy in SSE stream
2. SDK captures and normalizes to providerMetadata.energy
3. Processor stores as-is with source: 'measured'
4. Plugin hooks can enhance (e.g., add grid intensity)
1. Provider sends standard response (no energy)
2. Processor checks providerMetadata.energy → not present
3. Processor calls plugin hook: message.energy.estimate
4. If plugin provides estimate → use it
5. Else → use built-in estimation (model coefficients × tokens × PUE)
6. Store with source: 'estimated'
7. Plugin hooks can enhance (e.g., add real-time grid intensity)
type Energy = {
// Normalized energy values
wh?: number // Watt-hours
kwh?: number // Kilowatt-hours
joules?: number // Joules
// Carbon emissions
gCO2e?: number // Grams CO2 equivalent
// Metadata
source: 'measured' | 'estimated'
provider?: string // 'greenpt', 'neuralwatt', 'opencode', plugin name
method?: string // Calculation method
// Grid intensity (if carbon was calculated)
gridIntensity?: {
value: number // gCO2e/kWh
region: string // 'global', 'us-west', etc.
source?: string // 'electricitymaps', 'static'
}
// Raw provider data
raw?: Record<string, unknown>
}type StepFinishPart = {
// ... existing fields ...
type: 'step-finish'
tokens: { input, output, reasoning, cache }
cost: number
// NEW
energy?: Energy
}type AssistantMessage = {
// ... existing fields ...
tokens: { input, output, reasoning, cache }
cost: number
// NEW: Aggregated from all steps
energy?: Energy
}Called when provider doesn't supply measured energy. Plugins can provide custom estimates.
"message.energy.estimate": async (input, output) => {
// input: { sessionID, messageID, modelID, providerID, tokens }
// output: { energy?: Energy }
// Plugin sets output.energy to provide custom estimate
output.energy = {
kwh: myCustomCalculation(input.tokens, input.modelID),
source: 'estimated',
provider: 'my-plugin',
}
}Called after energy is determined (measured or estimated). Plugins can enhance or log.
"message.energy": async (input, output) => {
// input: { sessionID, messageID, modelID, providerID, tokens, region? }
// output: { energy: Energy }
// Enhance with real-time grid intensity
const grid = await fetchGridIntensity(input.region)
output.energy.gCO2e = output.energy.kwh * grid.intensity
output.energy.gridIntensity = { value: grid.intensity, ... }
// Log to external service
await logToHelicone(input.sessionID, output.energy)
}Published when energy data is stored. Plugins subscribed to event can react.
{
type: 'message.energy.updated',
properties: {
sessionID: string
messageID: string
partID?: string
energy: Energy
context: {
modelID: string
providerID: string
tokens: { input, output, reasoning }
}
}
}Extend existing tooltip to show energy:
┌────────────────────────────┐
│ 15,234 tokens │
│ 42% context usage │
│ $0.0234 cost │
│ ───────────────── │ ← NEW
│ 0.0047 Wh energy │ ← NEW
│ 0.0021 gCO₂e carbon │ ← NEW
│ ───────────────── │
│ Click to view details │
└────────────────────────────┘
Add energy totals to session summary:
Session Summary
├── Messages: 12
├── Tokens: 45,000 in / 8,000 out
├── Cost: $0.089
├── Energy: 0.024 Wh (estimated) ← NEW
└── Carbon: 0.010 gCO₂e ← NEW
- Add Energy type to message-v2.ts
- Extend StepFinishPart and AssistantMessage schemas
- Update processor to capture/estimate energy
- Regenerate SDK types
- Extend chunk schema for GreenPT impact field
- Add custom SSE handler for Neuralwatt comments
- Pass energy through providerMetadata
- Add
message.energy.estimatehook - Add
message.energyhook - Add
message.energy.updatedevent - Update Plugin.trigger calls in processor
- Update session-context-usage.tsx
- Add energy to message tooltips
- Add to session summary view
- Update plugin documentation
- Add energy tracking guide
- Document provider support
- All energy fields are optional
- Existing messages without energy continue to work
- Plugins that don't implement energy hooks are unaffected
- UI gracefully handles missing energy data
- ElectricityMaps integration - Real-time grid carbon intensity
- Regional configuration - User can set their region for better carbon estimates
- Energy budgets - Warn when session exceeds energy threshold
- Aggregated reporting - Daily/weekly energy usage reports
- Provider adoption - Encourage more providers to send measured energy
packages/opencode/src/provider/sdk/openai-compatible/src/responses/
└── openai-responses-language-model.ts # SDK energy capture
packages/opencode/src/session/
├── message-v2.ts # Schema changes
├── processor.ts # Energy capture/estimation
└── index.ts # getUsage updates
packages/plugin/src/
└── index.ts # New hooks
packages/sdk/js/src/gen/
└── types.gen.ts # Regenerated types
packages/app/src/components/
└── session-context-usage.tsx # UI display
- Should energy be opt-in or always calculated?
- Default grid intensity: global average or configurable?
- Should we show energy in the main UI or only in details?
- How to handle energy for tool calls vs. main response?
- Should session viewer be updated in core or as separate project?